Read Photoshop Large Documents, layerless files and oversized layers (12.11b1)

This commit is contained in:
雷电芽衣 committed 2026-09-29 04:23:27 -04:00
1 parent 8a9c56ee1f
commit a2241601ec
30 files changed
+1228 -385

No files matched your search

+1 -1
View File
@@ -87,7 +87,7 @@ void ContentView::showImporter()
m_importer = new QFileDialog(this);
m_importer->setAttribute(Qt::WA_DeleteOnClose);
m_importer->setFileMode(QFileDialog::ExistingFiles);
m_importer->setNameFilter(QStringLiteral("Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd %1)").arg(RawImporter::globs().join(' ')));
m_importer->setNameFilter(QStringLiteral("Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd *.psb %1)").arg(RawImporter::globs().join(' ')));
connect(m_importer, &QDialog::finished, this, [this, panel = m_importer.data()](int result) {
// Ours no longer: the flag must not reject it again.
if (m_importer != panel)
+35 -11
View File
@@ -100,23 +100,19 @@ void EditorSession::decodeNext()
drainImports();
return;
}
qint64 usedPixels = 0;
if (m_document) {
for (const ImageLayer &layer : m_document->layers) {
if (layer.asset)
usedPixels += qint64(layer.asset->size().width()) * layer.asset->size().height();
}
}
const QUrl url = request.urls[m_importIndex];
if (url.isLocalFile() && RawImporter::matches(url.toLocalFile())) {
decodeRaw(url.toLocalFile(), DocumentLimits::documentPixelBudget() - usedPixels);
decodeRaw(url.toLocalFile(), remainingPixels());
return;
}
if (url.isLocalFile() && PSDReader::matches(url.toLocalFile())) {
beginPSDReading(QStringLiteral("Open “%1”?").arg(url.fileName()), QStringLiteral("Import"));
m_photoshopReader.setFuture(QtConcurrent::run([path = url.toLocalFile(), remaining = DocumentLimits::documentPixelBudget() - usedPixels]() -> PhotoshopRead {
m_photoshopReader.setFuture(QtConcurrent::run([path = url.toLocalFile(), remaining = remainingPixels()]() -> PhotoshopRead {
try {
PSDDocument document = ImageImporter::loadPhotoshop(path, remaining);
// No layer records: Photoshop wrote the merged image alone.
if (document.layers.empty())
return {std::nullopt, {}, QString(), true};
std::map<QUuid, ImportedImage> assets = ImageImporter::photoshopAssets(document);
return {std::move(document), std::move(assets), QString()};
} catch (const ImageImportError &error) {
@@ -129,13 +125,35 @@ void EditorSession::decodeNext()
}));
return;
}
m_decoder.setFuture(QtConcurrent::run([url, remaining = DocumentLimits::documentPixelBudget() - usedPixels]() -> Decoded {
decode(url, false);
}
qint64 EditorSession::remainingPixels() const
{
qint64 usedPixels = 0;
if (m_document) {
for (const ImageLayer &layer : m_document->layers) {
if (layer.asset)
usedPixels += qint64(layer.asset->size().width()) * layer.asset->size().height();
}
}
return DocumentLimits::documentPixelBudget() - usedPixels;
}
// `flattened`: a layerless Photoshop file, read as its merged image.
void EditorSession::decode(const QUrl &url, bool flattened)
{
m_decoder.setFuture(QtConcurrent::run([url, flattened, remaining = remainingPixels()]() -> Decoded {
try {
if (!url.isLocalFile())
throw ImageImportError(ImageImportError::Kind::unsupported);
return {ImageImporter::decode(url.toLocalFile(), remaining), QString()};
return {ImageImporter::decode(url.toLocalFile(), remaining, flattened), QString()};
} catch (const ImageImportError &error) {
return {std::nullopt, url.fileName() + ": " + error.what()};
} catch (const PSDError &error) {
return {std::nullopt, url.fileName() + ": " + error.what()};
} catch (const ExportError &error) {
return {std::nullopt, url.fileName() + ": " + error.what()};
}
}));
}
@@ -210,6 +228,12 @@ void EditorSession::finishPhotoshopRead()
m_importIndex += 1;
decodeNext();
};
// Swift ends the reading first, then reads the merged image.
if (read.layerless) {
endPSDReading();
decode(url, true);
return;
}
// Every image has its asset: making layers cannot fail.
const std::optional<PSDImport> imported = read.document ? std::optional(PSDDocumentBuilder::makeImport(*read.document, read.assets)) : std::nullopt;
if (!imported) {
+2
View File
@@ -36,6 +36,8 @@ struct SessionJobs {
std::optional<PSDDocument> document;
std::map<QUuid, ImportedImage> assets;
QString failure;
// No layer records: a background alone, read merged next.
bool layerless = false;
};
struct Baked {
std::map<QUuid, ImportedImage> images;
+2
View File
@@ -330,6 +330,8 @@ private:
void updateBusyIndicator();
void drainImports();
void decodeNext();
qint64 remainingPixels() const;
void decode(const QUrl &url, bool flattened);
void finishDecode();
void finishPhotoshopRead();
void endDuplicateTransform();
+2 -2
View File
@@ -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
View File
@@ -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)
+2 -2
View File
@@ -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
View File
@@ -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);
}
+15 -1
View File
@@ -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);
+3
View File
@@ -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 &note : notes(record))
conversions.push_back(PSDConversion{.layerName = record.name, .message = note});
if (record.kind == PSDLayerKind::adjustment && !record.adjustment)
+294
View File
@@ -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));
}
+75
View File
@@ -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
View File
@@ -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 {
+3
View File
@@ -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;
}
+1 -1
View File
@@ -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.");
+2
View File
@@ -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;
};