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
@@ -8,12 +8,12 @@
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
// Swift's order: PNG, JPEG, HEIC, TIFF, Photoshop, RAW, then any.
|
||||
// Swift's order: PNG, JPEG, HEIC, TIFF, PSD, PSB, RAW, any.
|
||||
QStringList pictureFormats(const QMimeData &data)
|
||||
{
|
||||
QStringList formats;
|
||||
for (const QString &format : {QStringLiteral("image/png"), QStringLiteral("image/jpeg"), QStringLiteral("image/heic"), QStringLiteral("image/tiff"),
|
||||
QStringLiteral("image/vnd.adobe.photoshop")})
|
||||
QStringLiteral("image/vnd.adobe.photoshop"), QStringLiteral("image/x-photoshop-large-image")})
|
||||
if (data.hasFormat(format))
|
||||
formats << format;
|
||||
for (const QString &format : data.formats())
|
||||
|
||||
+36
-27
@@ -148,6 +148,37 @@ QImage decodeHEIC(const QString &path, qint64 remainingPixels)
|
||||
image.setColorSpace(stated);
|
||||
return image;
|
||||
}
|
||||
|
||||
// Into sRGB with the profile still attached, then turned upright.
|
||||
ImportedImage finish(QImage image, const QString &path, QImageIOHandler::Transformations orientation)
|
||||
{
|
||||
image.convertToColorSpace(QColorSpace::SRgb);
|
||||
image = std::move(image).convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||||
// Qt 6.9 renamed mirrored as flipped; the floor stays 6.4.
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
|
||||
Qt::Orientations flips;
|
||||
flips.setFlag(Qt::Horizontal, orientation.testFlag(QImageIOHandler::TransformationMirror));
|
||||
flips.setFlag(Qt::Vertical, orientation.testFlag(QImageIOHandler::TransformationFlip));
|
||||
image = std::move(image).flipped(flips);
|
||||
#else
|
||||
image = std::move(image).mirrored(orientation.testFlag(QImageIOHandler::TransformationMirror),
|
||||
orientation.testFlag(QImageIOHandler::TransformationFlip));
|
||||
#endif
|
||||
if (orientation.testFlag(QImageIOHandler::TransformationRotate90))
|
||||
image = image.transformed(QTransform().rotate(90));
|
||||
if (image.isNull())
|
||||
refuse(ImageImportError::Kind::unreadable, path, QStringLiteral("out of memory"));
|
||||
image.setColorSpace(QColorSpace::SRgb);
|
||||
// Swift: scale = min(1, 96 / longest), extent rounded outward.
|
||||
const qint64 longest = std::max(image.width(), image.height());
|
||||
const QSize small(int((image.width() * 96 + longest - 1) / longest), int((image.height() * 96 + longest - 1) / longest));
|
||||
const QImage thumbnail = longest <= 96 ? image : image.scaled(small, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
if (thumbnail.isNull())
|
||||
refuse(ImageImportError::Kind::unreadable, path, QStringLiteral("out of memory"));
|
||||
const QString name = QFileInfo(path).completeBaseName();
|
||||
qCInfo(lcIO).noquote() << "imported" << path << image.width() << "x" << image.height();
|
||||
return ImportedImage(image, thumbnail, name.isEmpty() ? QFileInfo(path).fileName() : name);
|
||||
}
|
||||
}
|
||||
|
||||
ImportedImage::ImportedImage(QImage image, QImage thumbnail, QString name)
|
||||
@@ -190,9 +221,12 @@ void ImageImporter::liftAllocationLimit()
|
||||
Q_UNUSED(lifted)
|
||||
}
|
||||
|
||||
ImportedImage ImageImporter::decode(const QString &path, qint64 remainingPixels)
|
||||
ImportedImage ImageImporter::decode(const QString &path, qint64 remainingPixels, bool flattenedPhotoshop)
|
||||
{
|
||||
liftAllocationLimit();
|
||||
// A Photoshop file of a background alone: its merged image.
|
||||
if (flattenedPhotoshop && PSDReader::matches(path))
|
||||
return finish(PSDReader::merged(path, remainingPixels), path, QImageIOHandler::TransformationNone);
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
refuse(ImageImportError::Kind::unreadable, path, file.errorString());
|
||||
@@ -219,32 +253,7 @@ ImportedImage ImageImporter::decode(const QString &path, qint64 remainingPixels)
|
||||
if (!reader.read(&image))
|
||||
refuse(ImageImportError::Kind::unreadable, path, reader.errorString());
|
||||
}
|
||||
image.convertToColorSpace(QColorSpace::SRgb);
|
||||
image = std::move(image).convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||||
// Qt 6.9 renamed mirrored as flipped; the floor stays 6.4.
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
|
||||
Qt::Orientations flips;
|
||||
flips.setFlag(Qt::Horizontal, orientation.testFlag(QImageIOHandler::TransformationMirror));
|
||||
flips.setFlag(Qt::Vertical, orientation.testFlag(QImageIOHandler::TransformationFlip));
|
||||
image = std::move(image).flipped(flips);
|
||||
#else
|
||||
image = std::move(image).mirrored(orientation.testFlag(QImageIOHandler::TransformationMirror),
|
||||
orientation.testFlag(QImageIOHandler::TransformationFlip));
|
||||
#endif
|
||||
if (orientation.testFlag(QImageIOHandler::TransformationRotate90))
|
||||
image = image.transformed(QTransform().rotate(90));
|
||||
if (image.isNull())
|
||||
refuse(ImageImportError::Kind::unreadable, path, QStringLiteral("out of memory"));
|
||||
image.setColorSpace(QColorSpace::SRgb);
|
||||
// Swift: scale = min(1, 96 / longest), extent rounded outward.
|
||||
const qint64 longest = std::max(image.width(), image.height());
|
||||
const QSize small(int((image.width() * 96 + longest - 1) / longest), int((image.height() * 96 + longest - 1) / longest));
|
||||
const QImage thumbnail = longest <= 96 ? image : image.scaled(small, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
if (thumbnail.isNull())
|
||||
refuse(ImageImportError::Kind::unreadable, path, QStringLiteral("out of memory"));
|
||||
const QString name = QFileInfo(path).completeBaseName();
|
||||
qCInfo(lcIO).noquote() << "imported" << path << image.width() << "x" << image.height();
|
||||
return ImportedImage(image, thumbnail, name.isEmpty() ? QFileInfo(path).fileName() : name);
|
||||
return finish(std::move(image), path, orientation);
|
||||
}
|
||||
|
||||
PSDDocument ImageImporter::loadPhotoshop(const QString &path, qint64 remainingPixels)
|
||||
|
||||
@@ -48,8 +48,8 @@ public:
|
||||
namespace ImageImporter {
|
||||
// Qt refuses decodes over 128 MB; the app's budgets rule.
|
||||
void liftAllocationLimit();
|
||||
// JPEG, PNG, TIFF or HEIC: upright, sRGB, premultiplied.
|
||||
ImportedImage decode(const QString &path, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
|
||||
// JPEG, PNG, TIFF, HEIC or a layerless PSD: upright sRGB.
|
||||
ImportedImage decode(const QString &path, qint64 remainingPixels = DocumentLimits::documentPixelBudget(), bool flattenedPhotoshop = false);
|
||||
PSDDocument loadPhotoshop(const QString &path, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
|
||||
std::map<QUuid, ImportedImage> photoshopAssets(const PSDDocument &document);
|
||||
}
|
||||
+106
-31
@@ -2,65 +2,140 @@
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "IO/PSD/PSDTypes.h"
|
||||
#include <QColorSpace>
|
||||
#include <span>
|
||||
|
||||
namespace {
|
||||
std::vector<uchar> unpackRLE(int width, int height, const QByteArray &data)
|
||||
{
|
||||
struct Reader {
|
||||
const QByteArray &data;
|
||||
qsizetype offset = 0;
|
||||
const auto next = [&]() -> uchar {
|
||||
uchar next()
|
||||
{
|
||||
if (offset >= data.size())
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
return uchar(data[offset++]);
|
||||
};
|
||||
std::vector<qsizetype> counts(static_cast<size_t>(height));
|
||||
for (int row = 0; row < height; ++row) {
|
||||
const uchar hi = next(), lo = next();
|
||||
counts[size_t(row)] = qsizetype(hi) << 8 | lo;
|
||||
}
|
||||
std::vector<uchar> plane(size_t(width) * size_t(height));
|
||||
for (int row = 0; row < height; ++row) {
|
||||
const qsizetype end = offset + counts[size_t(row)];
|
||||
if (end > data.size())
|
||||
// Two bytes a row, or four in a Large Document.
|
||||
qsizetype count(bool largeDocument)
|
||||
{
|
||||
qsizetype value = 0;
|
||||
for (int index = 0; index < (largeDocument ? 4 : 2); ++index)
|
||||
value = value << 8 | next();
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
// The rows' counts, first checked to fit in the data.
|
||||
std::vector<qsizetype> counts(Reader &reader, qint64 height, bool largeDocument)
|
||||
{
|
||||
if (height > (reader.data.size() - reader.offset) / (largeDocument ? 4 : 2))
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
std::vector<qsizetype> result(static_cast<size_t>(height));
|
||||
for (qsizetype &count : result)
|
||||
count = reader.count(largeDocument);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Only the crop's columns kept: no source-wide row.
|
||||
std::vector<uchar> unpackRows(Reader &reader, const std::vector<qsizetype> &counts, qint64 width, const PSDCrop &crop)
|
||||
{
|
||||
std::vector<uchar> plane(static_cast<size_t>(crop.width * crop.height));
|
||||
for (size_t index = 0; index < counts.size(); ++index) {
|
||||
const qint64 y = qint64(index);
|
||||
const qsizetype end = reader.offset + counts[index];
|
||||
if (end > reader.data.size())
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
int written = 0;
|
||||
uchar *line = plane.data() + size_t(row) * size_t(width);
|
||||
if (y < crop.y || y >= crop.y + crop.height) {
|
||||
reader.offset = end;
|
||||
continue;
|
||||
}
|
||||
// Checked: a row past the crop would overrun.
|
||||
const std::span<uchar> target = std::span(plane).subspan(static_cast<size_t>((y - crop.y) * crop.width), static_cast<size_t>(crop.width));
|
||||
// What of a run lies in the crop's columns.
|
||||
const auto keep = [&](qint64 from, qint64 count, const auto &put) {
|
||||
const qint64 first = std::max(from, crop.x), last = std::min(from + count, crop.x + crop.width);
|
||||
for (qint64 x = first; x < last; ++x)
|
||||
target[static_cast<size_t>(x - crop.x)] = put(x - from);
|
||||
};
|
||||
qint64 written = 0;
|
||||
while (written < width) {
|
||||
if (offset >= end)
|
||||
if (reader.offset >= end)
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
const int n = qint8(data[offset++]);
|
||||
const int n = qint8(reader.data[reader.offset++]);
|
||||
if (n >= 0) {
|
||||
const int count = n + 1;
|
||||
if (written + count > width || offset + count > end)
|
||||
const qint64 count = n + 1;
|
||||
if (written + count > width || reader.offset + count > end)
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
std::copy_n(data.constData() + offset, count, line + written);
|
||||
offset += count;
|
||||
const qsizetype at = reader.offset;
|
||||
keep(written, count, [&](qint64 i) { return uchar(reader.data[at + i]); });
|
||||
reader.offset += count;
|
||||
written += count;
|
||||
} else if (n != -128) {
|
||||
const int count = 1 - n;
|
||||
if (written + count > width || offset >= end)
|
||||
const qint64 count = 1 - n;
|
||||
if (written + count > width || reader.offset >= end)
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
std::fill_n(line + written, count, uchar(data[offset++]));
|
||||
const uchar value = uchar(reader.data[reader.offset++]);
|
||||
keep(written, count, [&](qint64) { return value; });
|
||||
written += count;
|
||||
}
|
||||
}
|
||||
offset = end;
|
||||
reader.offset = end;
|
||||
}
|
||||
return plane;
|
||||
}
|
||||
|
||||
std::vector<uchar> cropRaw(qint64 width, qint64 height, const QByteArray &data, qsizetype start, const PSDCrop &crop)
|
||||
{
|
||||
if (data.size() - start < width * height)
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
std::vector<uchar> plane(static_cast<size_t>(crop.width * crop.height));
|
||||
for (qint64 row = 0; row < crop.height; ++row)
|
||||
std::copy_n(data.constData() + start + (crop.y + row) * width + crop.x, crop.width, plane.data() + row * crop.width);
|
||||
return plane;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uchar> PSDChannelCoder::decode(int compression, int width, int height, const QByteArray &data)
|
||||
std::vector<uchar> PSDChannelCoder::decode(int compression, qint64 width, qint64 height, const QByteArray &data, bool largeDocument,
|
||||
const std::optional<PSDCrop> &crop)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
return {};
|
||||
const qsizetype expected = qsizetype(width) * height;
|
||||
const PSDCrop part = crop.value_or(PSDCrop{0, 0, width, height});
|
||||
if (part.x < 0 || part.y < 0 || part.width < 0 || part.height < 0 || part.x + part.width > width || part.y + part.height > height)
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
if (part.width == 0 || part.height == 0)
|
||||
return {};
|
||||
switch (compression) {
|
||||
case 0:
|
||||
if (data.size() < expected)
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
return std::vector<uchar>(data.constData(), data.constData() + expected);
|
||||
case 1:
|
||||
return unpackRLE(width, height, data);
|
||||
return cropRaw(width, height, data, 0, part);
|
||||
case 1: {
|
||||
Reader reader{data};
|
||||
return unpackRows(reader, counts(reader, height, largeDocument), width, part);
|
||||
}
|
||||
default:
|
||||
throw PSDError(PSDError::Kind::unsupportedCompression);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::vector<uchar>> PSDChannelCoder::mergedPlanes(int compression, int width, int height, int channels, int wanted, const QByteArray &data,
|
||||
bool largeDocument)
|
||||
{
|
||||
const PSDCrop whole{0, 0, width, height};
|
||||
std::vector<std::vector<uchar>> planes;
|
||||
switch (compression) {
|
||||
case 0:
|
||||
for (int channel = 0; channel < wanted; ++channel)
|
||||
planes.push_back(cropRaw(width, height, data, qsizetype(channel) * width * height, whole));
|
||||
return planes;
|
||||
case 1: {
|
||||
// Every channel's counts come first; only the wanted are unpacked.
|
||||
Reader reader{data};
|
||||
std::vector<std::vector<qsizetype>> table;
|
||||
for (int channel = 0; channel < channels; ++channel)
|
||||
table.push_back(counts(reader, height, largeDocument));
|
||||
for (int channel = 0; channel < wanted; ++channel)
|
||||
planes.push_back(unpackRows(reader, table[size_t(channel)], width, whole));
|
||||
return planes;
|
||||
}
|
||||
default:
|
||||
throw PSDError(PSDError::Kind::unsupportedCompression);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
#pragma once
|
||||
#include <QByteArray>
|
||||
#include <QImage>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
// What a channel keeps when cut; wide, as sources are.
|
||||
struct PSDCrop {
|
||||
qint64 x;
|
||||
qint64 y;
|
||||
qint64 width;
|
||||
qint64 height;
|
||||
};
|
||||
|
||||
// Photoshop's layer channels: raw and PackBits (Adobe's 2019 specification).
|
||||
namespace PSDChannelCoder {
|
||||
std::vector<uchar> decode(int compression, int width, int height, const QByteArray &data);
|
||||
// A Large Document counts PackBits rows in four bytes.
|
||||
std::vector<uchar> decode(int compression, qint64 width, qint64 height, const QByteArray &data, bool largeDocument = false,
|
||||
const std::optional<PSDCrop> &crop = std::nullopt);
|
||||
// The merged image's first `wanted` of its `channels` planes.
|
||||
std::vector<std::vector<uchar>> mergedPlanes(int compression, int width, int height, int channels, int wanted, const QByteArray &data,
|
||||
bool largeDocument);
|
||||
// Straight planes into premultiplied RGBA, sRGB.
|
||||
QImage rgbaImage(int width, int height, const std::vector<uchar> &red, const std::vector<uchar> &green,
|
||||
const std::vector<uchar> &blue, const std::vector<uchar> &alpha);
|
||||
|
||||
@@ -80,6 +80,9 @@ PSDImport PSDDocumentBuilder::makeImport(const PSDDocument &document, const std:
|
||||
std::vector<ImageLayer> layers;
|
||||
const QSizeF canvas(document.width, document.height);
|
||||
for (const PSDRecord &record : document.layers) {
|
||||
if (record.croppedToCanvas)
|
||||
conversions.push_back(PSDConversion{.layerName = record.name,
|
||||
.message = QStringLiteral("Cropped to the canvas so the file fits in memory. Pixels outside the canvas weren't imported.")});
|
||||
for (const QString ¬e : notes(record))
|
||||
conversions.push_back(PSDConversion{.layerName = record.name, .message = note});
|
||||
if (record.kind == PSDLayerKind::adjustment && !record.adjustment)
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
#include "IO/PSD/PSDReader+Records.h"
|
||||
#include "Document/DocumentLimits.h"
|
||||
#include "IO/ImageImporter.h"
|
||||
#include <set>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::optional<QString> unicodeName(const QByteArray &data)
|
||||
{
|
||||
if (data.size() < 4)
|
||||
return std::nullopt;
|
||||
const qint64 count = PSDRecords::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);
|
||||
}
|
||||
|
||||
// Keys whose lengths a Large Document writes in eight bytes.
|
||||
const std::set<QString> psbLargeKeys{
|
||||
QStringLiteral("LMsk"), QStringLiteral("Lr16"), QStringLiteral("Lr32"), QStringLiteral("Layr"), QStringLiteral("Mt16"),
|
||||
QStringLiteral("Mt32"), QStringLiteral("Mtrn"), QStringLiteral("Alph"), QStringLiteral("FMsk"), QStringLiteral("lnk2"),
|
||||
QStringLiteral("FEid"), QStringLiteral("FXid"), QStringLiteral("PxSD"),
|
||||
};
|
||||
|
||||
// Transparency, red, green, blue and the user mask only.
|
||||
bool unpacked(int id)
|
||||
{
|
||||
return id == -1 || id == 0 || id == 1 || id == 2 || id == -2;
|
||||
}
|
||||
|
||||
// Pixels and mask each within the side and budget.
|
||||
bool fits(qint64 width, qint64 height, qint64 maskWidth, qint64 maskHeight, bool hasMask, qint64 remainingPixels)
|
||||
{
|
||||
const qint64 budget = std::max<qint64>(0, remainingPixels);
|
||||
if (width > 0 && height > 0 && (width > DocumentLimits::maxSide || height > DocumentLimits::maxSide || width * height > budget))
|
||||
return false;
|
||||
return !(hasMask && maskWidth > 0 && maskHeight > 0
|
||||
&& (maskWidth > DocumentLimits::maxSide || maskHeight > DocumentLimits::maxSide || maskWidth * maskHeight > budget));
|
||||
}
|
||||
|
||||
// A box's part on the canvas, from its own corner.
|
||||
PSDCrop crop(qint64 left, qint64 top, qint64 right, qint64 bottom, qint64 canvasWidth, qint64 canvasHeight)
|
||||
{
|
||||
const qint64 croppedLeft = std::min(canvasWidth, std::max<qint64>(0, left)), croppedTop = std::min(canvasHeight, std::max<qint64>(0, top));
|
||||
const qint64 croppedRight = std::max(croppedLeft, std::min(canvasWidth, right)), croppedBottom = std::max(croppedTop, std::min(canvasHeight, bottom));
|
||||
return PSDCrop{croppedLeft - left, croppedTop - top, croppedRight - croppedLeft, croppedBottom - croppedTop};
|
||||
}
|
||||
}
|
||||
|
||||
// Swift's `.ascii` decoding: any byte past 127 reads as nothing.
|
||||
QString PSDRecords::ascii(const QByteArray &bytes)
|
||||
{
|
||||
for (const char byte : bytes)
|
||||
if (uchar(byte) > 127)
|
||||
return QString();
|
||||
return QString::fromLatin1(bytes);
|
||||
}
|
||||
|
||||
quint32 PSDRecords::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]);
|
||||
}
|
||||
|
||||
void PSDRecords::Cursor::need(qint64 count) const
|
||||
{
|
||||
if (offset < 0 || offset > data.size() || count > data.size() - offset)
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
}
|
||||
|
||||
void PSDRecords::Cursor::skip(qint64 count)
|
||||
{
|
||||
if (count < 0)
|
||||
throw PSDError(PSDError::Kind::truncated);
|
||||
need(count);
|
||||
offset += count;
|
||||
}
|
||||
|
||||
uchar PSDRecords::Cursor::u8()
|
||||
{
|
||||
need(1);
|
||||
return uchar(data[offset++]);
|
||||
}
|
||||
|
||||
quint16 PSDRecords::Cursor::u16()
|
||||
{
|
||||
const quint16 high = u8();
|
||||
return quint16(high << 8 | u8());
|
||||
}
|
||||
|
||||
quint32 PSDRecords::Cursor::u32()
|
||||
{
|
||||
const quint32 high = u16();
|
||||
return high << 16 | u16();
|
||||
}
|
||||
|
||||
quint64 PSDRecords::Cursor::u64()
|
||||
{
|
||||
const quint64 high = u32();
|
||||
return high << 32 | u32();
|
||||
}
|
||||
|
||||
qint64 PSDRecords::Cursor::length(bool isPSB)
|
||||
{
|
||||
const quint64 value = isPSB ? u64() : u32();
|
||||
if (value > quint64(std::numeric_limits<qint64>::max()))
|
||||
throw ImageImportError(ImageImportError::Kind::tooLarge);
|
||||
return qint64(value);
|
||||
}
|
||||
|
||||
QByteArray PSDRecords::Cursor::bytes(qint64 count)
|
||||
{
|
||||
need(count);
|
||||
offset += count;
|
||||
return data.mid(offset - count, count);
|
||||
}
|
||||
|
||||
PSDRecords::RawLayer PSDRecords::readRecord(Cursor &cursor, bool isPSB)
|
||||
{
|
||||
RawLayer layer;
|
||||
layer.top = cursor.i32();
|
||||
layer.left = cursor.i32();
|
||||
layer.bottom = cursor.i32();
|
||||
layer.right = cursor.i32();
|
||||
layer.sourceTop = layer.top, layer.sourceLeft = layer.left, layer.sourceBottom = layer.bottom, layer.sourceRight = layer.right;
|
||||
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.length(isPSB)});
|
||||
}
|
||||
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();
|
||||
layer.sourceMaskTop = layer.maskTop, layer.sourceMaskLeft = layer.maskLeft;
|
||||
layer.sourceMaskBottom = layer.maskBottom, layer.sourceMaskRight = layer.maskRight;
|
||||
// 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") || (isPSB && psbLargeKeys.contains(key))) {
|
||||
if (cursor.offset + 8 > extraEnd)
|
||||
break;
|
||||
length = cursor.length(true);
|
||||
} 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;
|
||||
}
|
||||
|
||||
bool PSDRecords::fitsBudget(const std::vector<RawLayer> &layers, qint64 remainingPixels)
|
||||
{
|
||||
qint64 usedPixels = 0;
|
||||
for (const RawLayer &layer : layers) {
|
||||
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);
|
||||
if (!fits(width, height, maskWidth, maskHeight, layer.hasMask, remainingPixels - usedPixels))
|
||||
return false;
|
||||
if (width > 0 && height > 0)
|
||||
usedPixels += width * height;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PSDRecords::cropToCanvas(RawLayer &layer, qint64 width, qint64 height)
|
||||
{
|
||||
const PSDCrop image = crop(layer.left, layer.top, layer.right, layer.bottom, width, height);
|
||||
if (image.x != 0 || image.y != 0 || image.width != layer.right - layer.left || image.height != layer.bottom - layer.top) {
|
||||
layer.left += image.x;
|
||||
layer.top += image.y;
|
||||
layer.right = layer.left + image.width;
|
||||
layer.bottom = layer.top + image.height;
|
||||
layer.imageCrop = image;
|
||||
layer.cropped = true;
|
||||
}
|
||||
if (!layer.hasMask)
|
||||
return;
|
||||
const PSDCrop mask = crop(layer.maskLeft, layer.maskTop, layer.maskRight, layer.maskBottom, width, height);
|
||||
if (mask.x != 0 || mask.y != 0 || mask.width != layer.maskRight - layer.maskLeft || mask.height != layer.maskBottom - layer.maskTop) {
|
||||
layer.maskLeft += mask.x;
|
||||
layer.maskTop += mask.y;
|
||||
layer.maskRight = layer.maskLeft + mask.width;
|
||||
layer.maskBottom = layer.maskTop + mask.height;
|
||||
layer.maskCrop = mask;
|
||||
layer.cropped = true;
|
||||
}
|
||||
}
|
||||
|
||||
void PSDRecords::decodeChannels(Cursor &cursor, RawLayer &layer, qint64 remainingPixels, bool isPSB)
|
||||
{
|
||||
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);
|
||||
if (!fits(width, height, maskWidth, maskHeight, layer.hasMask, remainingPixels))
|
||||
throw ImageImportError(ImageImportError::Kind::tooLarge);
|
||||
const qint64 sourceWidth = std::max<qint64>(0, layer.sourceRight - layer.sourceLeft), sourceHeight = std::max<qint64>(0, layer.sourceBottom - layer.sourceTop);
|
||||
const qint64 sourceMaskWidth = std::max<qint64>(0, layer.sourceMaskRight - layer.sourceMaskLeft);
|
||||
const qint64 sourceMaskHeight = std::max<qint64>(0, layer.sourceMaskBottom - layer.sourceMaskTop);
|
||||
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;
|
||||
// Written at the source bounds; a cut decodes part.
|
||||
const qint64 w = isMask ? maskWidth : width, h = isMask ? maskHeight : height;
|
||||
if (w > 0 && h > 0)
|
||||
planes[channel.id] = PSDChannelCoder::decode(compression, isMask ? sourceMaskWidth : sourceWidth, isMask ? sourceMaskHeight : sourceHeight,
|
||||
payload, isPSB, isMask ? layer.maskCrop : layer.imageCrop);
|
||||
}
|
||||
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));
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
#include "IO/PSD/PSDChannelCoder.h"
|
||||
#include "IO/PSD/PSDTypes.h"
|
||||
#include <QByteArray>
|
||||
#include <QImage>
|
||||
#include <QString>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
// Swift's PSDReader, split: the layer records and pixels.
|
||||
namespace PSDRecords {
|
||||
QString ascii(const QByteArray &bytes);
|
||||
quint32 u32(const QByteArray &data, qsizetype at);
|
||||
|
||||
struct Cursor {
|
||||
const QByteArray &data;
|
||||
qint64 offset = 0;
|
||||
|
||||
void need(qint64 count) const;
|
||||
void skip(qint64 count);
|
||||
uchar u8();
|
||||
quint16 u16();
|
||||
qint16 i16() { return qint16(u16()); }
|
||||
quint32 u32();
|
||||
qint32 i32() { return qint32(u32()); }
|
||||
quint64 u64();
|
||||
// A Large Document's lengths take eight bytes.
|
||||
qint64 length(bool isPSB);
|
||||
QByteArray bytes(qint64 count);
|
||||
QString string(qint64 count) { return ascii(bytes(count)); }
|
||||
};
|
||||
|
||||
struct Channel {
|
||||
int id;
|
||||
qint64 length;
|
||||
};
|
||||
|
||||
struct RawLayer {
|
||||
QString name;
|
||||
qint64 top = 0, left = 0, bottom = 0, right = 0;
|
||||
// The bounds the channels were written at, before any crop.
|
||||
qint64 sourceTop = 0, sourceLeft = 0, sourceBottom = 0, sourceRight = 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;
|
||||
qint64 sourceMaskTop = 0, sourceMaskLeft = 0, sourceMaskBottom = 0, sourceMaskRight = 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;
|
||||
std::optional<PSDCrop> imageCrop;
|
||||
std::optional<PSDCrop> maskCrop;
|
||||
bool cropped = false;
|
||||
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)); });
|
||||
}
|
||||
};
|
||||
|
||||
RawLayer readRecord(Cursor &cursor, bool isPSB);
|
||||
// Every layer within the side limit and the budget.
|
||||
bool fitsBudget(const std::vector<RawLayer> &layers, qint64 remainingPixels);
|
||||
// Cuts a layer and mask to the canvas, marking it.
|
||||
void cropToCanvas(RawLayer &layer, qint64 width, qint64 height);
|
||||
void decodeChannels(Cursor &cursor, RawLayer &layer, qint64 remainingPixels, bool isPSB);
|
||||
}
|
||||
+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 {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "Document/DocumentLimits.h"
|
||||
#include "IO/PSD/PSDTypes.h"
|
||||
#include <QByteArray>
|
||||
#include <QImage>
|
||||
#include <QString>
|
||||
#include <map>
|
||||
#include <set>
|
||||
@@ -12,6 +13,8 @@ bool matches(const QString &path);
|
||||
bool matches(const QByteArray &data);
|
||||
PSDDocument read(const QString &path, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
|
||||
PSDDocument read(const QByteArray &data, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
|
||||
// A file with no layers: its merged image, profile attached.
|
||||
QImage merged(const QString &path, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
|
||||
extern const std::set<QString> adjustmentKeys;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ QString description(PSDError::Kind kind)
|
||||
case PSDError::Kind::truncated:
|
||||
return QStringLiteral("The Photoshop file could not be read. It may be damaged or incomplete.");
|
||||
case PSDError::Kind::unsupportedVersion:
|
||||
return QStringLiteral("Large Document (.psb) Photoshop files aren’t supported.");
|
||||
return QStringLiteral("This Photoshop file uses a format version OmaPhoto can’t read.");
|
||||
case PSDError::Kind::unsupportedColorMode:
|
||||
case PSDError::Kind::unsupportedDepth:
|
||||
return QStringLiteral("Only 8-bit RGB Photoshop files can be imported.");
|
||||
|
||||
@@ -43,6 +43,8 @@ struct PSDRecord {
|
||||
PSDLayerKind kind = PSDLayerKind::raster;
|
||||
std::optional<LayerShapeStyle> shape;
|
||||
std::vector<QString> shapeNotes;
|
||||
// Cut to the canvas so the file fits the budget.
|
||||
bool croppedToCanvas = false;
|
||||
std::optional<LayerBlendMode> blendMode() const;
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user