255 lines
11 KiB
C++
255 lines
11 KiB
C++
#include "AddressSpaceLimit.h"
|
|
#include "Document/EditorSession.h"
|
|
#include "IO/ImageImporter.h"
|
|
#include "IO/PSD/PSDChannelCoder.h"
|
|
#include "IO/PSD/PSDReader.h"
|
|
#include "PSDFixture.h"
|
|
#include <QColorSpace>
|
|
#include <QTemporaryDir>
|
|
#include <QtTest>
|
|
|
|
// Swift 1.2.10: Large Documents, and files of a background alone.
|
|
namespace {
|
|
QString written(const QTemporaryDir &folder, const QString &name, const QByteArray &bytes)
|
|
{
|
|
QFile file(folder.filePath(name));
|
|
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size())
|
|
throw std::runtime_error("could not write a fixture");
|
|
return file.fileName();
|
|
}
|
|
|
|
// Photoshop's ICC resource, 1039, added after the others.
|
|
QByteArray withProfile(QByteArray file, const QByteArray &profile)
|
|
{
|
|
PSDFixture::Buffer resource;
|
|
resource.string("8BIM");
|
|
resource.u16(1039);
|
|
resource.u16(0);
|
|
resource.u32(quint32(profile.size()));
|
|
resource.bytes(profile);
|
|
if (profile.size() % 2 == 1)
|
|
resource.u8(0);
|
|
// The resources' length sits after the header and colour data.
|
|
const quint32 length = quint32(uchar(file[30])) << 24 | quint32(uchar(file[31])) << 16 | quint32(uchar(file[32])) << 8 | uchar(file[33]);
|
|
PSDFixture::Buffer grown;
|
|
grown.u32(length + quint32(resource.data.size()));
|
|
file.replace(30, 4, grown.data);
|
|
file.insert(34 + qsizetype(length), resource.data);
|
|
return file;
|
|
}
|
|
}
|
|
|
|
class PSBImportTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void PSBRoundTripMatchesPSDLayerContent();
|
|
void PSBExceedingCanvasLimitIsRejected();
|
|
void PSBLargeAdditionalInfoBlockDoesNotHideUnicodeName();
|
|
void aLayerlessFileReadsAsItsMergedImage();
|
|
void theMergedImageTakesItsProfileAndTheBudget();
|
|
void aLayerlessFileImportsAsOneLayer();
|
|
void aMergedImageIsJudgedBeforeTheFileIsRead();
|
|
void theReadingEndsBeforeTheMergedRead();
|
|
void aLayeredReadSkipsTheProfile();
|
|
};
|
|
|
|
void PSBImportTests::PSBRoundTripMatchesPSDLayerContent()
|
|
{
|
|
PSDRecord bottom = PSDFixture::record(QStringLiteral("Red"), PSDFixture::colorImage(3, 2, 1, 0, 0), QRectF(0, 0, 3, 2));
|
|
PSDRecord middle = PSDFixture::record(QStringLiteral("Green"), PSDFixture::colorImage(2, 3, 0, 1, 0), QRectF(1, 1, 2, 3));
|
|
PSDRecord top = PSDFixture::record(QStringLiteral("Blue mask"), PSDFixture::colorImage(2, 2, 0, 0, 1), QRectF(2, 2, 2, 2));
|
|
top.mask = PSDFixture::grayImage(2, 2, 128);
|
|
const PSDDocument source{5, 5, 144, {bottom, middle, top}};
|
|
const QImage composite = PSDFixture::colorImage(5, 5, 0, 0, 0);
|
|
const PSDDocument psd = PSDReader::read(PSDFixture::data(source, composite));
|
|
const PSDDocument psb = PSDReader::read(PSDFixture::data(source, composite, true));
|
|
QCOMPARE(psb.layers.size(), psd.layers.size());
|
|
for (size_t index = 0; index < psd.layers.size(); ++index) {
|
|
QCOMPARE(psb.layers[index].name, psd.layers[index].name);
|
|
QCOMPARE(psb.layers[index].bounds, psd.layers[index].bounds);
|
|
QCOMPARE(psb.layers[index].image.value(), psd.layers[index].image.value());
|
|
}
|
|
QCOMPARE(psb.layers[2].mask.value(), psd.layers[2].mask.value());
|
|
QCOMPARE(psb.resolution, 144.0);
|
|
}
|
|
|
|
void PSBImportTests::PSBExceedingCanvasLimitIsRejected()
|
|
{
|
|
const QImage image = PSDFixture::colorImage(2, 2, 1, 0, 0);
|
|
QByteArray data = PSDFixture::data(PSDDocument{2, 2, 72, {PSDFixture::record(QStringLiteral("Large"), image, QRectF(0, 0, 2, 2))}}, image, true);
|
|
data.replace(18, 4, QByteArray("\0\0\x75\x31", 4));
|
|
try {
|
|
PSDReader::read(data);
|
|
QFAIL("a Large Document past the side limit was read");
|
|
} catch (const ImageImportError &error) {
|
|
QCOMPARE(error.kind, ImageImportError::Kind::tooLarge);
|
|
}
|
|
}
|
|
|
|
void PSBImportTests::PSBLargeAdditionalInfoBlockDoesNotHideUnicodeName()
|
|
{
|
|
const QImage image = PSDFixture::colorImage(2, 2, 1, 0, 0);
|
|
const PSDRecord layer = PSDFixture::record(QStringLiteral("Café layer"), image, QRectF(0, 0, 2, 2));
|
|
const QByteArray data =
|
|
PSDFixture::data(PSDDocument{2, 2, 72, {layer}}, image, true, PSDFixture::AdditionalLayerInfo{"LMsk", QByteArray("\1\2\3", 3)});
|
|
QCOMPARE(PSDReader::read(data).layers.at(0).name, QStringLiteral("Café layer"));
|
|
}
|
|
|
|
void PSBImportTests::aLayerlessFileReadsAsItsMergedImage()
|
|
{
|
|
// No layer records; each channel's rows pack apart.
|
|
const QTemporaryDir folder;
|
|
std::vector<uchar> red, green, blue;
|
|
for (int index = 0; index < 15; ++index) {
|
|
red.push_back(uchar(index * 17));
|
|
green.push_back(uchar(index < 8 ? 40 : 200));
|
|
blue.push_back(uchar(90));
|
|
}
|
|
const QImage composite = PSDChannelCoder::rgbaImage(5, 3, red, green, blue, std::vector<uchar>(15, 255));
|
|
for (const bool large : {false, true}) {
|
|
const QString path = written(folder, large ? "Flat.psb" : "Flat.psd", PSDFixture::data(PSDDocument{5, 3, 72, {}}, composite, large));
|
|
QVERIFY(PSDReader::read(path).layers.empty());
|
|
const QImage merged = PSDReader::merged(path);
|
|
QCOMPARE(merged, composite);
|
|
QCOMPARE(merged.colorSpace(), QColorSpace(QColorSpace::SRgb));
|
|
}
|
|
}
|
|
|
|
void PSBImportTests::theMergedImageTakesItsProfileAndTheBudget()
|
|
{
|
|
const QTemporaryDir folder;
|
|
const QImage composite = PSDFixture::colorImage(5, 3, 0.5, 0.25, 0.125);
|
|
const QByteArray flat = PSDFixture::data(PSDDocument{5, 3, 72, {}}, composite);
|
|
// Fifteen pixels against fourteen: refused before any pixel.
|
|
try {
|
|
PSDReader::merged(written(folder, "Flat.psd", flat), 14);
|
|
QFAIL("a merged image past the budget was read");
|
|
} catch (const ImageImportError &error) {
|
|
QCOMPARE(error.kind, ImageImportError::Kind::tooLarge);
|
|
}
|
|
// Past one surface yet in budget: read on, truncated.
|
|
if (DocumentLimits::documentPixelBudget() > qint64(20'001) * 10'000) {
|
|
PSDFixture::Buffer header;
|
|
header.string("8BPS");
|
|
header.u16(1);
|
|
header.bytes(QByteArray(6, '\0'));
|
|
header.u16(3);
|
|
header.u32(10'000);
|
|
header.u32(20'001);
|
|
header.u16(8);
|
|
header.u16(3);
|
|
header.bytes(QByteArray(12, '\0'));
|
|
header.u16(1);
|
|
try {
|
|
PSDReader::merged(written(folder, "Wide.psd", header.data));
|
|
QFAIL("a header alone was read");
|
|
} catch (const PSDError &error) {
|
|
QCOMPARE(error.kind, PSDError::Kind::truncated);
|
|
}
|
|
}
|
|
// Display P3 numbers arrive as the sRGB they name.
|
|
const QString tagged = written(folder, "P3.psd", withProfile(flat, QColorSpace(QColorSpace::DisplayP3).iccProfile()));
|
|
QCOMPARE(PSDReader::merged(tagged).colorSpace(), QColorSpace(QColorSpace::DisplayP3));
|
|
const ImportedImage asset = ImageImporter::decode(tagged, DocumentLimits::documentPixelBudget(), true);
|
|
const QColor expected = QColorSpace(QColorSpace::DisplayP3).transformationToColorSpace(QColorSpace::SRgb).map(composite.pixelColor(0, 0));
|
|
const QColor got = asset.image().pixelColor(0, 0);
|
|
QVERIFY2(std::abs(got.red() - expected.red()) <= 1 && std::abs(got.green() - expected.green()) <= 1 && std::abs(got.blue() - expected.blue()) <= 1,
|
|
qPrintable(got.name() + " " + expected.name()));
|
|
QVERIFY(got != composite.pixelColor(0, 0));
|
|
}
|
|
|
|
void PSBImportTests::aLayerlessFileImportsAsOneLayer()
|
|
{
|
|
const QTemporaryDir folder;
|
|
const QImage composite = PSDFixture::colorImage(5, 3, 0, 0.5, 1);
|
|
const QString path = written(folder, "Background.psb", PSDFixture::data(PSDDocument{5, 3, 72, {}}, composite, true));
|
|
EditorSession session;
|
|
session.createDocument(8, 8);
|
|
bool done = false;
|
|
session.importImages({QUrl::fromLocalFile(path)}, QPointF(6, 5), [&done] { done = true; });
|
|
QVERIFY(QTest::qWaitFor([&done] { return done; }, 20'000));
|
|
// No conversion sheet: the merged image comes straight in.
|
|
QVERIFY(!session.showsConversionSheet() && !session.importError());
|
|
QCOMPARE(session.document().value().layers.size(), size_t(1));
|
|
const ImageLayer layer = session.activeLayer().value();
|
|
QCOMPARE(layer.name, QString("Background"));
|
|
QCOMPARE(layer.asset.value().image().pixelColor(2, 1), composite.pixelColor(2, 1));
|
|
// Dropped at a point, it lands there.
|
|
const QPointF centre = layer.transform.center();
|
|
QVERIFY2(std::abs(centre.x() - 6) <= 0.5 && std::abs(centre.y() - 5) <= 0.5, qPrintable(QString("%1, %2").arg(centre.x()).arg(centre.y())));
|
|
}
|
|
|
|
void PSBImportTests::aMergedImageIsJudgedBeforeTheFileIsRead()
|
|
{
|
|
// Sparse 75 MB planes: past a budget of one.
|
|
const QTemporaryDir folder;
|
|
for (const bool large : {false, true}) {
|
|
PSDFixture::Buffer header;
|
|
header.string("8BPS");
|
|
header.u16(large ? 2 : 1);
|
|
header.bytes(QByteArray(6, '\0'));
|
|
header.u16(3);
|
|
header.u32(5'000);
|
|
header.u32(5'000);
|
|
header.u16(8);
|
|
header.u16(3);
|
|
header.bytes(QByteArray(large ? 16 : 12, '\0'));
|
|
header.u16(0);
|
|
const QString path = written(folder, large ? "Big.psb" : "Big.psd", header.data);
|
|
QVERIFY(QFile(path).resize(header.data.size() + 75'000'000));
|
|
QVERIFY(PSDReader::read(path, 1).layers.empty());
|
|
std::optional<ImageImportError::Kind> kind;
|
|
{
|
|
const AddressSpaceLimit limit(32ll * 1024 * 1024);
|
|
try {
|
|
ImageImporter::decode(path, 1, true);
|
|
} catch (const ImageImportError &error) {
|
|
kind = error.kind;
|
|
}
|
|
}
|
|
QCOMPARE(kind, std::optional(ImageImportError::Kind::tooLarge));
|
|
}
|
|
}
|
|
|
|
void PSBImportTests::theReadingEndsBeforeTheMergedRead()
|
|
{
|
|
// Gone once the reading ends: Swift reads the merged after.
|
|
const QTemporaryDir folder;
|
|
const QString path = written(folder, "Gone.psd", PSDFixture::data(PSDDocument{5, 3, 72, {}}, PSDFixture::colorImage(5, 3, 1, 0, 0)));
|
|
EditorSession session;
|
|
session.createDocument(8, 8);
|
|
bool reading = false;
|
|
connect(&session, &EditorSession::changed, this, [&] {
|
|
reading = reading || session.showsConversionSheet();
|
|
if (reading && !session.showsConversionSheet() && QFile::exists(path))
|
|
QFile::remove(path);
|
|
});
|
|
bool done = false;
|
|
session.importImages({QUrl::fromLocalFile(path)}, std::nullopt, [&done] { done = true; });
|
|
QVERIFY(QTest::qWaitFor([&done] { return done; }, 20'000));
|
|
QVERIFY(reading && !QFile::exists(path));
|
|
QVERIFY(session.document().value().layers.empty());
|
|
QCOMPARE(session.importError(), std::optional("Gone.psd: " + QString(ImageImportError(ImageImportError::Kind::unreadable).what())));
|
|
}
|
|
|
|
void PSBImportTests::aLayeredReadSkipsTheProfile()
|
|
{
|
|
// A 64 MB profile, never copied while layers are read.
|
|
const QImage image = PSDFixture::colorImage(2, 2, 1, 0, 0);
|
|
for (const bool large : {false, true}) {
|
|
const QByteArray file = withProfile(PSDFixture::data(PSDDocument{2, 2, 72, {PSDFixture::record(QStringLiteral("Red"), image, QRectF(0, 0, 2, 2))}}, image, large),
|
|
QByteArray(64 * 1024 * 1024, 'x'));
|
|
std::optional<PSDDocument> read;
|
|
{
|
|
const AddressSpaceLimit limit(32ll * 1024 * 1024);
|
|
read = PSDReader::read(file);
|
|
}
|
|
QCOMPARE(read.value().layers.size(), size_t(1));
|
|
QCOMPARE(read.value().layers[0].image.value(), PSDReader::read(file).layers[0].image.value());
|
|
QCOMPARE(read.value().layers[0].image.value().pixel(1, 1), qRgb(255, 0, 0));
|
|
}
|
|
}
|
|
|
|
QTEST_MAIN(PSBImportTests)
|
|
#include "PSBImportTests.moc"
|