Read Photoshop Large Documents, layerless files and oversized layers (12.11b1)
This commit is contained in:
1 parent
8a9c56ee1f
commit
a2241601ec
30 files changed
+1228
-385
No files matched your search
+95
-257
@@ -1,253 +1,14 @@
|
||||
#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 {
|
||||
// Bytes 0x80–0xFF in Mac OS Roman, as Python's `mac_roman`.
|
||||
constexpr char16_t macRoman[128] = {
|
||||
0x00C4, 0x00C5, 0x00C7, 0x00C9, 0x00D1, 0x00D6, 0x00DC, 0x00E1, 0x00E0, 0x00E2, 0x00E4, 0x00E3,
|
||||
0x00E5, 0x00E7, 0x00E9, 0x00E8, 0x00EA, 0x00EB, 0x00ED, 0x00EC, 0x00EE, 0x00EF, 0x00F1, 0x00F3,
|
||||
0x00F2, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x00F9, 0x00FB, 0x00FC, 0x2020, 0x00B0, 0x00A2, 0x00A3,
|
||||
0x00A7, 0x2022, 0x00B6, 0x00DF, 0x00AE, 0x00A9, 0x2122, 0x00B4, 0x00A8, 0x2260, 0x00C6, 0x00D8,
|
||||
0x221E, 0x00B1, 0x2264, 0x2265, 0x00A5, 0x00B5, 0x2202, 0x2211, 0x220F, 0x03C0, 0x222B, 0x00AA,
|
||||
0x00BA, 0x03A9, 0x00E6, 0x00F8, 0x00BF, 0x00A1, 0x00AC, 0x221A, 0x0192, 0x2248, 0x2206, 0x00AB,
|
||||
0x00BB, 0x2026, 0x00A0, 0x00C0, 0x00C3, 0x00D5, 0x0152, 0x0153, 0x2013, 0x2014, 0x201C, 0x201D,
|
||||
0x2018, 0x2019, 0x00F7, 0x25CA, 0x00FF, 0x0178, 0x2044, 0x20AC, 0x2039, 0x203A, 0xFB01, 0xFB02,
|
||||
0x2021, 0x00B7, 0x201A, 0x201E, 0x2030, 0x00C2, 0x00CA, 0x00C1, 0x00CB, 0x00C8, 0x00CD, 0x00CE,
|
||||
0x00CF, 0x00CC, 0x00D3, 0x00D4, 0xF8FF, 0x00D2, 0x00DA, 0x00DB, 0x00D9, 0x0131, 0x02C6, 0x02DC,
|
||||
0x00AF, 0x02D8, 0x02D9, 0x02DA, 0x00B8, 0x02DD, 0x02DB, 0x02C7,
|
||||
};
|
||||
|
||||
QString fromMacRoman(const QByteArray &bytes)
|
||||
{
|
||||
QString text;
|
||||
text.reserve(bytes.size());
|
||||
for (const char byte : bytes)
|
||||
text += uchar(byte) < 0x80 ? QChar(uchar(byte)) : QChar(macRoman[uchar(byte) - 0x80]);
|
||||
return text;
|
||||
}
|
||||
|
||||
// Swift's `.ascii` decoding: any byte past 127 reads as nothing.
|
||||
QString ascii(const QByteArray &bytes)
|
||||
{
|
||||
for (const char byte : bytes)
|
||||
if (uchar(byte) > 127)
|
||||
return QString();
|
||||
return QString::fromLatin1(bytes);
|
||||
}
|
||||
|
||||
struct Cursor {
|
||||
const QByteArray &data;
|
||||
qint64 offset = 0;
|
||||
|
||||
void need(qint64 count) const
|
||||
{
|
||||
if (offset < 0 || offset > data.size() || count > data.size() - offset)
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
}
|
||||
void skip(qint64 count)
|
||||
{
|
||||
if (count < 0)
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
need(count);
|
||||
offset += count;
|
||||
}
|
||||
uchar u8()
|
||||
{
|
||||
need(1);
|
||||
return uchar(data[offset++]);
|
||||
}
|
||||
quint16 u16()
|
||||
{
|
||||
const quint16 high = u8();
|
||||
return quint16(high << 8 | u8());
|
||||
}
|
||||
qint16 i16() { return qint16(u16()); }
|
||||
quint32 u32()
|
||||
{
|
||||
const quint32 high = u16();
|
||||
return high << 16 | u16();
|
||||
}
|
||||
qint32 i32() { return qint32(u32()); }
|
||||
QByteArray bytes(qint64 count)
|
||||
{
|
||||
need(count);
|
||||
offset += count;
|
||||
return data.mid(offset - count, count);
|
||||
}
|
||||
QString string(qint64 count) { return ascii(bytes(count)); }
|
||||
};
|
||||
|
||||
quint32 u32(const QByteArray &data, qsizetype at)
|
||||
{
|
||||
return quint32(uchar(data[at])) << 24 | quint32(uchar(data[at + 1])) << 16 | quint32(uchar(data[at + 2])) << 8 | uchar(data[at + 3]);
|
||||
}
|
||||
|
||||
struct Channel {
|
||||
int id;
|
||||
qint64 length;
|
||||
};
|
||||
|
||||
struct RawLayer {
|
||||
QString name;
|
||||
qint64 top = 0, left = 0, bottom = 0, right = 0;
|
||||
uchar opacity = 255;
|
||||
uchar fill = 255;
|
||||
bool clipping = false;
|
||||
bool hidden = false;
|
||||
QString blendKey = QStringLiteral("norm");
|
||||
std::vector<Channel> channels;
|
||||
std::map<QString, QByteArray> extra;
|
||||
qint64 maskTop = 0, maskLeft = 0, maskBottom = 0, maskRight = 0;
|
||||
bool maskDisabled = false;
|
||||
bool maskLinked = true;
|
||||
bool maskFromRender = false;
|
||||
bool hasMask = false;
|
||||
std::optional<qint64> section;
|
||||
std::optional<QImage> image;
|
||||
std::optional<QImage> maskImage;
|
||||
bool has(std::initializer_list<const char *> keys) const
|
||||
{
|
||||
return std::any_of(keys.begin(), keys.end(), [this](const char *key) { return extra.contains(QString::fromLatin1(key)); });
|
||||
}
|
||||
};
|
||||
|
||||
std::optional<QString> unicodeName(const QByteArray &data)
|
||||
{
|
||||
if (data.size() < 4)
|
||||
return std::nullopt;
|
||||
const qint64 count = u32(data, 0);
|
||||
if (count <= 0 || data.size() < 4 + count * 2)
|
||||
return std::nullopt;
|
||||
QString name(qsizetype(count), Qt::Uninitialized);
|
||||
for (qint64 i = 0; i < count; ++i)
|
||||
name[qsizetype(i)] = QChar(quint16(uchar(data[4 + i * 2]) << 8 | uchar(data[5 + i * 2])));
|
||||
// Swift trims NUL from both ends.
|
||||
qsizetype from = 0, to = name.size();
|
||||
while (from < to && name[from] == QChar(0))
|
||||
++from;
|
||||
while (to > from && name[to - 1] == QChar(0))
|
||||
--to;
|
||||
return name.mid(from, to - from);
|
||||
}
|
||||
|
||||
RawLayer readRecord(Cursor &cursor)
|
||||
{
|
||||
RawLayer layer;
|
||||
layer.top = cursor.i32();
|
||||
layer.left = cursor.i32();
|
||||
layer.bottom = cursor.i32();
|
||||
layer.right = cursor.i32();
|
||||
const int channelCount = cursor.u16();
|
||||
if (channelCount > 56)
|
||||
throw ImageImportError(ImageImportError::Kind::tooLarge);
|
||||
for (int index = 0; index < channelCount; ++index) {
|
||||
const int id = cursor.i16();
|
||||
layer.channels.push_back({id, cursor.u32()});
|
||||
}
|
||||
if (cursor.string(4) != QLatin1String("8BIM"))
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
layer.blendKey = cursor.string(4);
|
||||
layer.opacity = cursor.u8();
|
||||
layer.clipping = cursor.u8() != 0;
|
||||
layer.hidden = (cursor.u8() & 2) != 0;
|
||||
cursor.skip(1);
|
||||
const qint64 extraLength = cursor.u32();
|
||||
const qint64 extraEnd = cursor.offset + extraLength;
|
||||
const qint64 maskLength = cursor.u32();
|
||||
const qint64 maskEnd = cursor.offset + maskLength;
|
||||
if (maskLength >= 20) {
|
||||
layer.hasMask = true;
|
||||
layer.maskTop = cursor.i32();
|
||||
layer.maskLeft = cursor.i32();
|
||||
layer.maskBottom = cursor.i32();
|
||||
layer.maskRight = cursor.i32();
|
||||
// The mask's default colour is skipped, as Swift ignores it.
|
||||
cursor.skip(1);
|
||||
const uchar flags = cursor.u8();
|
||||
layer.maskDisabled = (flags & 2) != 0;
|
||||
layer.maskLinked = (flags & 1) == 0;
|
||||
layer.maskFromRender = (flags & 8) != 0;
|
||||
}
|
||||
cursor.offset = maskEnd;
|
||||
cursor.skip(cursor.u32());
|
||||
const int nameCount = cursor.u8();
|
||||
layer.name = fromMacRoman(cursor.bytes(nameCount));
|
||||
cursor.skip((4 - ((nameCount + 1) % 4)) % 4);
|
||||
while (cursor.offset + 12 <= extraEnd) {
|
||||
const QString signature = cursor.string(4);
|
||||
if (signature != QLatin1String("8BIM") && signature != QLatin1String("8B64"))
|
||||
break;
|
||||
const QString key = cursor.string(4);
|
||||
qint64 length;
|
||||
if (signature == QLatin1String("8B64")) {
|
||||
if (cursor.offset + 8 > extraEnd)
|
||||
break;
|
||||
const quint64 raw = quint64(cursor.u32()) << 32 | cursor.u32();
|
||||
if (raw > quint64(std::numeric_limits<qint64>::max()))
|
||||
throw ImageImportError(ImageImportError::Kind::tooLarge);
|
||||
length = qint64(raw);
|
||||
} else {
|
||||
length = cursor.u32();
|
||||
}
|
||||
const QByteArray payload = cursor.bytes(length);
|
||||
if (length % 2 == 1)
|
||||
cursor.skip(1);
|
||||
layer.extra[key] = payload;
|
||||
if (key == QLatin1String("luni"))
|
||||
if (const std::optional<QString> unicode = unicodeName(payload))
|
||||
layer.name = *unicode;
|
||||
if (key == QLatin1String("iOpa") && !payload.isEmpty())
|
||||
layer.fill = uchar(payload[0]);
|
||||
if ((key == QLatin1String("lsct") || key == QLatin1String("lsdk")) && payload.size() >= 4)
|
||||
layer.section = u32(payload, 0);
|
||||
}
|
||||
cursor.offset = extraEnd;
|
||||
return layer;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Transparency, red, green, blue and the user mask only.
|
||||
bool unpacked(int id)
|
||||
{
|
||||
return id == -1 || id == 0 || id == 1 || id == 2 || id == -2;
|
||||
}
|
||||
|
||||
void decodeChannels(Cursor &cursor, RawLayer &layer, qint64 remainingPixels)
|
||||
{
|
||||
std::map<int, std::vector<uchar>> planes;
|
||||
const qint64 width = std::max<qint64>(0, layer.right - layer.left), height = std::max<qint64>(0, layer.bottom - layer.top);
|
||||
const qint64 maskWidth = std::max<qint64>(0, layer.maskRight - layer.maskLeft), maskHeight = std::max<qint64>(0, layer.maskBottom - layer.maskTop);
|
||||
const qint64 budget = std::max<qint64>(0, remainingPixels);
|
||||
if (width > 0 && height > 0 && (width > DocumentLimits::maxSide || height > DocumentLimits::maxSide || width * height > budget))
|
||||
throw ImageImportError(ImageImportError::Kind::tooLarge);
|
||||
if (layer.hasMask && maskWidth > 0 && maskHeight > 0 && (maskWidth > DocumentLimits::maxSide || maskHeight > DocumentLimits::maxSide || maskWidth * maskHeight > budget))
|
||||
throw ImageImportError(ImageImportError::Kind::tooLarge);
|
||||
for (const Channel &channel : layer.channels) {
|
||||
const qint64 start = cursor.offset;
|
||||
if (unpacked(channel.id) && channel.length >= 2) {
|
||||
const int compression = cursor.u16();
|
||||
const QByteArray payload = cursor.bytes(channel.length - 2);
|
||||
const bool isMask = channel.id == -2;
|
||||
const qint64 w = isMask ? maskWidth : width, h = isMask ? maskHeight : height;
|
||||
if (w > 0 && h > 0)
|
||||
planes[channel.id] = PSDChannelCoder::decode(compression, int(w), int(h), payload);
|
||||
}
|
||||
cursor.offset = start + std::max<qint64>(0, channel.length);
|
||||
}
|
||||
// Each plane decodes to exactly its size: no count check.
|
||||
if (layer.hasMask && maskWidth > 0 && maskHeight > 0 && planes.contains(-2))
|
||||
layer.maskImage = PSDChannelCoder::maskImage(int(maskWidth), int(maskHeight), planes.at(-2));
|
||||
if (width <= 0 || height <= 0)
|
||||
return;
|
||||
const size_t count = size_t(width * height);
|
||||
const auto plane = [&](int id, uchar fill) { return planes.contains(id) ? planes.at(id) : std::vector<uchar>(count, fill); };
|
||||
layer.image = PSDChannelCoder::rgbaImage(int(width), int(height), plane(0, 0), plane(1, 0), plane(2, 0), plane(-1, 255));
|
||||
}
|
||||
using namespace PSDRecords;
|
||||
|
||||
PSDLayerKind kindOf(const RawLayer &layer, bool isGroup)
|
||||
{
|
||||
@@ -295,6 +56,7 @@ std::vector<PSDRecord> assemble(const std::vector<RawLayer> &raw, QSizeF canvas,
|
||||
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));
|
||||
@@ -329,18 +91,32 @@ std::vector<PSDRecord> assemble(const std::vector<RawLayer> &raw, QSizeF canvas,
|
||||
return result;
|
||||
}
|
||||
|
||||
PSDDocument parse(const QByteArray &data, qint64 remainingPixels)
|
||||
// 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)
|
||||
{
|
||||
Cursor cursor{data};
|
||||
if (cursor.string(4) != QLatin1String("8BPS"))
|
||||
throw ImageImportError(ImageImportError::Kind::unreadable);
|
||||
if (cursor.u16() != 1)
|
||||
// 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);
|
||||
cursor.u16();
|
||||
const qint64 canvasHeight = cursor.u32(), canvasWidth = cursor.u32();
|
||||
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 (canvasWidth < 1 || canvasWidth > DocumentLimits::maxSide || canvasHeight < 1 || canvasHeight > DocumentLimits::maxSide || canvasWidth * canvasHeight > DocumentLimits::maxSurfacePixels)
|
||||
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);
|
||||
@@ -349,7 +125,6 @@ PSDDocument parse(const QByteArray &data, qint64 remainingPixels)
|
||||
cursor.skip(cursor.u32());
|
||||
const qint64 resourcesLength = cursor.u32();
|
||||
const qint64 resourcesEnd = cursor.offset + resourcesLength;
|
||||
double resolution = 72;
|
||||
while (cursor.offset + 12 <= resourcesEnd) {
|
||||
if (cursor.string(4) != QLatin1String("8BIM"))
|
||||
break;
|
||||
@@ -361,35 +136,75 @@ PSDDocument parse(const QByteArray &data, qint64 remainingPixels)
|
||||
const qint64 length = cursor.u32();
|
||||
const qint64 dataStart = cursor.offset;
|
||||
if (id == 1005 && length >= 4) {
|
||||
resolution = cursor.u32() / 65536.0;
|
||||
header.resolution = cursor.u32() / 65536.0;
|
||||
// A fixed-point number is finite; below one reads as 72.
|
||||
resolution = std::min(9600.0, resolution < 1 ? 72 : resolution);
|
||||
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;
|
||||
const qint64 layerSection = cursor.u32();
|
||||
const PSDDocument empty{int(canvasWidth), int(canvasHeight), resolution, {}};
|
||||
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.u32();
|
||||
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));
|
||||
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);
|
||||
decodeChannels(cursor, layer, remainingPixels - usedPixels, header.isPSB);
|
||||
if (layer.image)
|
||||
usedPixels += qint64(layer.image->width()) * layer.image->height();
|
||||
}
|
||||
return PSDDocument{int(canvasWidth), int(canvasHeight), resolution, assemble(raw, QSizeF(canvasWidth, canvasHeight), remainingPixels - usedPixels)};
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,6 +243,29 @@ PSDDocument PSDReader::read(const QString &path, qint64 remainingPixels)
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user