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
@@ -323,7 +323,7 @@ void ContentViewTests::theImporterPicksFilesAndClearsItsFlag()
|
||||
QTest::qWait(50);
|
||||
QCOMPARE(int(session.document().value().layers.size()), 2);
|
||||
desk.typeBeforeCancel.clear();
|
||||
const QString panel = "panel|Open|open|file|||1 many Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd " + RawImporter::globs().join(" ") + ")";
|
||||
const QString panel = "panel|Open|open|file|||1 many Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd *.psb " + RawImporter::globs().join(" ") + ")";
|
||||
QCOMPARE(desk.seen.mid(0, 2), (QStringList{panel, panel}));
|
||||
QCOMPARE(desk.seen.size(), 3);
|
||||
// The flag cleared from elsewhere takes the panel down.
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
#include "IO/PSD/PSDChannelCoder.h"
|
||||
#include "IO/PSD/PSDDocumentBuilder.h"
|
||||
#include "IO/PSD/PSDReader.h"
|
||||
#include "AddressSpaceLimit.h"
|
||||
#include "PSDReaderFixtures.h"
|
||||
#include <QtTest>
|
||||
|
||||
// Swift 1.2.10 (1fcf854): oversized layers cut to the canvas.
|
||||
namespace {
|
||||
QImage rgbaImage(int width, int height)
|
||||
{
|
||||
std::vector<uchar> red, green, blue, alpha;
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
red.push_back(uchar(x * 30 + y));
|
||||
green.push_back(uchar(y * 50));
|
||||
blue.push_back(uchar(255 - x * 20));
|
||||
alpha.push_back(255);
|
||||
}
|
||||
}
|
||||
return PSDChannelCoder::rgbaImage(width, height, red, green, blue, alpha);
|
||||
}
|
||||
|
||||
QImage grayImage(int width, int height)
|
||||
{
|
||||
std::vector<uchar> gray;
|
||||
for (int index = 0; index < width * height; ++index)
|
||||
gray.push_back(uchar(index * 10));
|
||||
return PSDChannelCoder::maskImage(width, height, gray);
|
||||
}
|
||||
|
||||
// A masked layer hanging off a 4 by 4 canvas.
|
||||
PSDDocument overhang()
|
||||
{
|
||||
PSDRecord layer = PSDFixture::record(QStringLiteral("Overhang"), rgbaImage(6, 3), QRectF(-2, 1, 6, 3));
|
||||
layer.mask = grayImage(6, 3);
|
||||
return PSDDocument{4, 4, 72, {layer}};
|
||||
}
|
||||
|
||||
QByteArray file(const PSDDocument &document)
|
||||
{
|
||||
return PSDFixture::data(document, rgbaImage(4, 4));
|
||||
}
|
||||
|
||||
bool cropNoted(const PSDDocument &parsed, const QString &name)
|
||||
{
|
||||
const PSDImport imported = PSDDocumentBuilder::makeImport(parsed, PSDDocumentBuilder::assets(parsed));
|
||||
return std::any_of(imported.conversions.begin(), imported.conversions.end(), [&](const PSDConversion ¬e) {
|
||||
return note.layerName == name && note.message.contains(QStringLiteral("Cropped to the canvas"));
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<uchar> sliced(const std::vector<uchar> &plane, int width, const PSDCrop &crop)
|
||||
{
|
||||
std::vector<uchar> result;
|
||||
for (int row = 0; row < crop.height; ++row)
|
||||
result.insert(result.end(), plane.begin() + (crop.y + row) * width + crop.x, plane.begin() + (crop.y + row) * width + crop.x + crop.width);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class CropToCanvasImportTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void fittingDocumentKeepsOversizedLayerPixels();
|
||||
void overBudgetDocumentCropsImageAndMaskToCanvas();
|
||||
void croppedDocumentStillOverBudgetIsRejected();
|
||||
void offCanvasLayerIsKeptWithoutPixelsWhenCropping();
|
||||
void channelCropsMatchFullDecodes();
|
||||
void theLayersAreSummedBeforeAnyCut();
|
||||
void aSourcePastIntStaysWide();
|
||||
void theCutIsJudgedBeforeAnyPixel();
|
||||
void anInvertedRecordIsCutToNothing();
|
||||
};
|
||||
|
||||
void CropToCanvasImportTests::fittingDocumentKeepsOversizedLayerPixels()
|
||||
{
|
||||
const PSDDocument source = overhang();
|
||||
const PSDDocument parsed = PSDReader::read(file(source), 100);
|
||||
const PSDRecord &layer = parsed.layers.at(0);
|
||||
QCOMPARE(layer.bounds, QRectF(-2, 1, 6, 3));
|
||||
QVERIFY(!layer.croppedToCanvas);
|
||||
QCOMPARE(layer.image.value(), source.layers[0].image.value());
|
||||
QVERIFY(!cropNoted(parsed, QStringLiteral("Overhang")));
|
||||
}
|
||||
|
||||
void CropToCanvasImportTests::overBudgetDocumentCropsImageAndMaskToCanvas()
|
||||
{
|
||||
// Eighteen pixels are past twelve; the canvas's twelve fit.
|
||||
const PSDDocument source = overhang();
|
||||
const PSDDocument parsed = PSDReader::read(file(source), 12);
|
||||
const PSDRecord &layer = parsed.layers.at(0);
|
||||
QCOMPARE(layer.bounds, QRectF(0, 1, 4, 3));
|
||||
QVERIFY(layer.croppedToCanvas);
|
||||
QCOMPARE(layer.image.value(), source.layers[0].image.value().copy(2, 0, 4, 3));
|
||||
QCOMPARE(layer.mask.value(), source.layers[0].mask.value().copy(2, 0, 4, 3));
|
||||
QVERIFY(cropNoted(parsed, QStringLiteral("Overhang")));
|
||||
}
|
||||
|
||||
void CropToCanvasImportTests::croppedDocumentStillOverBudgetIsRejected()
|
||||
{
|
||||
try {
|
||||
PSDReader::read(file(overhang()), 11);
|
||||
QFAIL("a file past the budget once cut was read");
|
||||
} catch (const ImageImportError &error) {
|
||||
QCOMPARE(error.kind, ImageImportError::Kind::tooLarge);
|
||||
}
|
||||
}
|
||||
|
||||
void CropToCanvasImportTests::offCanvasLayerIsKeptWithoutPixelsWhenCropping()
|
||||
{
|
||||
// Past the right edge, and wholly before the left.
|
||||
const PSDRecord right = PSDFixture::record(QStringLiteral("Outside"), rgbaImage(2, 2), QRectF(5, 0, 2, 2));
|
||||
const PSDRecord left = PSDFixture::record(QStringLiteral("Before"), rgbaImage(2, 2), QRectF(-5, 0, 2, 2));
|
||||
const PSDDocument parsed = PSDReader::read(file(PSDDocument{4, 4, 72, {right, left}}), 1);
|
||||
for (const PSDRecord &layer : parsed.layers)
|
||||
QVERIFY(!layer.image && layer.croppedToCanvas);
|
||||
QVERIFY(cropNoted(parsed, QStringLiteral("Outside")) && cropNoted(parsed, QStringLiteral("Before")));
|
||||
}
|
||||
|
||||
void CropToCanvasImportTests::channelCropsMatchFullDecodes()
|
||||
{
|
||||
const int width = 5, height = 4;
|
||||
QByteArray source;
|
||||
for (int index = 0; index < width * height; ++index)
|
||||
source.append(char(index));
|
||||
const PSDCrop crop{1, 1, 3, 2};
|
||||
const std::vector<uchar> raw = PSDChannelCoder::decode(0, width, height, source);
|
||||
QCOMPARE(PSDChannelCoder::decode(0, width, height, source, false, crop), sliced(raw, width, crop));
|
||||
for (const bool large : {false, true}) {
|
||||
std::vector<uchar> plane(source.begin(), source.end());
|
||||
const QByteArray packed = PSDFixture::encode(plane, width, height, large);
|
||||
const std::vector<uchar> full = PSDChannelCoder::decode(1, width, height, packed, large);
|
||||
QCOMPARE(full, raw);
|
||||
QCOMPARE(PSDChannelCoder::decode(1, width, height, packed, large, crop), sliced(full, width, crop));
|
||||
}
|
||||
}
|
||||
|
||||
void CropToCanvasImportTests::theLayersAreSummedBeforeAnyCut()
|
||||
{
|
||||
// Each fits thirty alone, not both: cut, they do.
|
||||
PSDDocument document = overhang();
|
||||
document.layers[0].mask.reset();
|
||||
document.layers.insert(document.layers.begin(), PSDFixture::record(QStringLiteral("Whole"), rgbaImage(4, 4), QRectF(0, 0, 4, 4)));
|
||||
const PSDDocument parsed = PSDReader::read(file(document), 30);
|
||||
QVERIFY(!parsed.layers.at(0).croppedToCanvas && parsed.layers.at(1).croppedToCanvas);
|
||||
QCOMPARE(parsed.layers.at(1).bounds, QRectF(0, 1, 4, 3));
|
||||
}
|
||||
|
||||
void CropToCanvasImportTests::aSourcePastIntStaysWide()
|
||||
{
|
||||
// A source 2^31 wide, cut to a one-pixel canvas.
|
||||
for (const char *payload : {"\0\0", "\0\1"}) {
|
||||
PSDReaderFixtures::Record record;
|
||||
record.bounds = QRect(0, 0, 1, 1);
|
||||
record.channels = {{0, QByteArray(payload, 2)}};
|
||||
QByteArray data = PSDReaderFixtures::file({record}, {}, 1, 1);
|
||||
data.replace(48, 4, QByteArray::fromHex("ffffffff"));
|
||||
data.replace(56, 4, QByteArray::fromHex("7fffffff"));
|
||||
// As tall too: its row counts judged before held.
|
||||
data.replace(44, 4, QByteArray::fromHex("ffffffff"));
|
||||
data.replace(52, 4, QByteArray::fromHex("7fffffff"));
|
||||
std::optional<PSDError::Kind> kind;
|
||||
{
|
||||
// Nothing the size of the source may be held.
|
||||
const AddressSpaceLimit limit(64ll * 1024 * 1024);
|
||||
try {
|
||||
PSDReader::read(data, 1);
|
||||
} catch (const PSDError &error) {
|
||||
kind = error.kind;
|
||||
}
|
||||
}
|
||||
QCOMPARE(kind, std::optional(PSDError::Kind::truncated));
|
||||
}
|
||||
}
|
||||
|
||||
void CropToCanvasImportTests::theCutIsJudgedBeforeAnyPixel()
|
||||
{
|
||||
// Two pixels in one: refused before the truncated first.
|
||||
PSDReaderFixtures::Record first, second;
|
||||
first.bounds = second.bounds = QRect(0, 0, 1, 1);
|
||||
first.channels = {{0, QByteArray("\0\0", 2)}};
|
||||
second.channels = {{0, QByteArray("\0\0\x80", 3)}};
|
||||
try {
|
||||
PSDReader::read(PSDReaderFixtures::file({first, second}, {}, 1, 1), 1);
|
||||
QFAIL("two pixels were read in a budget of one");
|
||||
} catch (const ImageImportError &error) {
|
||||
QCOMPARE(error.kind, ImageImportError::Kind::tooLarge);
|
||||
}
|
||||
}
|
||||
|
||||
void CropToCanvasImportTests::anInvertedRecordIsCutToNothing()
|
||||
{
|
||||
// Right before left: cut to nothing, and reported.
|
||||
PSDReaderFixtures::Record inverted, wide;
|
||||
inverted.bounds = QRect(2, 0, -1, 1);
|
||||
wide.bounds = QRect(-1, 0, 5, 1);
|
||||
PSDReaderFixtures::solidChannels(wide.bounds, wide.channels);
|
||||
const PSDDocument parsed = PSDReader::read(PSDReaderFixtures::file({inverted, wide}, {}, 4, 1), 4);
|
||||
QVERIFY(parsed.layers.at(0).croppedToCanvas && parsed.layers.at(1).croppedToCanvas);
|
||||
QCOMPARE(parsed.layers.at(0).bounds, QRectF(2, 0, 0, 1));
|
||||
QCOMPARE(parsed.layers.at(1).bounds, QRectF(0, 0, 4, 1));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(CropToCanvasImportTests)
|
||||
#include "CropToCanvasImportTests.moc"
|
||||
@@ -70,7 +70,7 @@ void DesktopIntegrationTests::theEntryOpensSwiftsDocumentTypes()
|
||||
QCOMPARE(keys.value("Terminal"), QString("false"));
|
||||
QVERIFY(keys.value("Categories").split(QLatin1Char(';')).contains("Graphics"));
|
||||
// Swift's two document types: its projects, then four pictures.
|
||||
QCOMPARE(keys.value("MimeType"), QString("application/x-compositor-project;image/png;image/jpeg;image/heic;image/tiff;image/vnd.adobe.photoshop;image/x-dcraw;"));
|
||||
QCOMPARE(keys.value("MimeType"), QString("application/x-compositor-project;image/png;image/jpeg;image/heic;image/tiff;image/vnd.adobe.photoshop;image/x-photoshop-large-image;image/x-dcraw;"));
|
||||
}
|
||||
|
||||
void DesktopIntegrationTests::aProjectFolderIsAType()
|
||||
@@ -87,9 +87,15 @@ void DesktopIntegrationTests::aProjectFolderIsAType()
|
||||
seen << "glob:" + xml.attributes().value("pattern").toString();
|
||||
else if (name == QLatin1String("comment"))
|
||||
seen << "comment:" + xml.readElementText();
|
||||
else if (name == QLatin1String("magic"))
|
||||
seen << "magic:" + xml.attributes().value("priority").toString();
|
||||
else if (name == QLatin1String("match"))
|
||||
seen << "match:" + xml.attributes().value("value").toString() + "@" + xml.attributes().value("offset").toString();
|
||||
}
|
||||
QVERIFY2(!xml.hasError(), qPrintable(xml.errorString()));
|
||||
QCOMPARE(seen, (QStringList{"mime-type:application/x-compositor-project", "comment:OmaPhoto Project", "sub-class-of:inode/directory", "glob:*.comp"}));
|
||||
QCOMPARE(seen, (QStringList{"mime-type:application/x-compositor-project", "comment:OmaPhoto Project", "sub-class-of:inode/directory", "glob:*.comp",
|
||||
"mime-type:image/x-photoshop-large-image", "comment:Photoshop Large Document", "glob:*.psb", "magic:50",
|
||||
"match:8BPS\\x00\\x02@0"}));
|
||||
}
|
||||
|
||||
void DesktopIntegrationTests::anInstallLaysDownTheAppItsEntryIconsAndLicense()
|
||||
|
||||
@@ -154,9 +154,13 @@ void ImageFileDropTests::aPictureIsCopiedToAFileFirst()
|
||||
raw.setData("image/gif", "any picture last");
|
||||
raw.setData("image/x-adobe-dng", DNGFixture::data({}));
|
||||
QCOMPARE(ImageFileDrop::providers(raw).front()->formats(), QStringList{"image/x-adobe-dng"});
|
||||
// A Large Document: after a PSD, before RAW and others.
|
||||
raw.setData("image/x-photoshop-large-image", "large first");
|
||||
QCOMPARE(ImageFileDrop::providers(raw).front()->formats(), QStringList{"image/x-photoshop-large-image"});
|
||||
raw.setData("image/vnd.adobe.photoshop", "photoshop first");
|
||||
QCOMPARE(ImageFileDrop::providers(raw).front()->formats(), QStringList{"image/vnd.adobe.photoshop"});
|
||||
raw.removeFormat("image/vnd.adobe.photoshop");
|
||||
raw.removeFormat("image/x-photoshop-large-image");
|
||||
EditorSession developed;
|
||||
done = false;
|
||||
ImageFileDrop::importProviders(raw, developed, std::nullopt, nullptr, std::nullopt, [&done] { done = true; });
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
#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"
|
||||
+60
-42
@@ -20,6 +20,13 @@ struct Buffer {
|
||||
u16(quint16(value));
|
||||
}
|
||||
void i32(qint32 value) { u32(quint32(value)); }
|
||||
void u64(quint64 value)
|
||||
{
|
||||
u32(quint32(value >> 32));
|
||||
u32(quint32(value));
|
||||
}
|
||||
// A Large Document's lengths are eight bytes.
|
||||
void length(quint64 value, bool large) { large ? u64(value) : u32(quint32(value)); }
|
||||
void bytes(const QByteArray &value) { data.append(value); }
|
||||
void string(const char *value) { data.append(value); }
|
||||
};
|
||||
@@ -96,21 +103,21 @@ inline QByteArray packBits(const std::vector<uchar> &row)
|
||||
}
|
||||
|
||||
// PackBits by row: the counts, then the rows.
|
||||
inline QByteArray encode(const std::vector<uchar> &plane, int width, int height)
|
||||
inline QByteArray encode(const std::vector<uchar> &plane, int width, int height, bool large = false)
|
||||
{
|
||||
Buffer counts;
|
||||
QByteArray packed;
|
||||
for (int row = 0; row < height; ++row) {
|
||||
const QByteArray line = packBits(std::vector<uchar>(plane.begin() + row * width, plane.begin() + (row + 1) * width));
|
||||
counts.u16(quint16(line.size()));
|
||||
large ? counts.u32(quint32(line.size())) : counts.u16(quint16(line.size()));
|
||||
packed.append(line);
|
||||
}
|
||||
return counts.data + packed;
|
||||
}
|
||||
|
||||
inline QByteArray channelPayload(const std::vector<uchar> &plane, int width, int height)
|
||||
inline QByteArray channelPayload(const std::vector<uchar> &plane, int width, int height, bool large = false)
|
||||
{
|
||||
return QByteArray("\0\1", 2) + encode(plane, width, height);
|
||||
return QByteArray("\0\1", 2) + encode(plane, width, height, large);
|
||||
}
|
||||
|
||||
inline std::vector<Channel> emptyChannels()
|
||||
@@ -119,7 +126,7 @@ inline std::vector<Channel> emptyChannels()
|
||||
return {{-1, raw}, {0, raw}, {1, raw}, {2, raw}};
|
||||
}
|
||||
|
||||
inline Prepared layer(const PSDRecord &record)
|
||||
inline Prepared layer(const PSDRecord &record, bool large = false)
|
||||
{
|
||||
const int width = record.image ? record.image->width() : 0, height = record.image ? record.image->height() : 0;
|
||||
const int left = int(std::round(record.bounds.left())), top = int(std::round(record.bounds.top()));
|
||||
@@ -128,12 +135,12 @@ inline Prepared layer(const PSDRecord &record)
|
||||
if (record.image && width > 0 && height > 0) {
|
||||
const Planes split = planes(*record.image);
|
||||
for (const auto &[id, plane] : {std::pair<qint16, const std::vector<uchar> &>(-1, split.alpha), {0, split.red}, {1, split.green}, {2, split.blue}})
|
||||
prepared.channels.push_back({id, channelPayload(plane, width, height)});
|
||||
prepared.channels.push_back({id, channelPayload(plane, width, height, large)});
|
||||
} else {
|
||||
prepared.channels = emptyChannels();
|
||||
}
|
||||
if (record.mask)
|
||||
prepared.channels.push_back({-2, channelPayload(grayPlane(*record.mask), record.mask->width(), record.mask->height())});
|
||||
prepared.channels.push_back({-2, channelPayload(grayPlane(*record.mask), record.mask->width(), record.mask->height(), large)});
|
||||
prepared.top = top;
|
||||
prepared.left = left;
|
||||
prepared.bottom = top + height;
|
||||
@@ -145,7 +152,7 @@ inline Prepared layer(const PSDRecord &record)
|
||||
return prepared;
|
||||
}
|
||||
|
||||
inline Prepared emptyLayer(const QString &name, const QString &blendKey, int section, std::optional<QUuid> parent, const PSDRecord *group = nullptr)
|
||||
inline Prepared emptyLayer(const QString &name, const QString &blendKey, int section, std::optional<QUuid> parent, const PSDRecord *group, bool large)
|
||||
{
|
||||
PSDRecord record;
|
||||
record.id = group ? group->id : QUuid::createUuid();
|
||||
@@ -163,18 +170,25 @@ inline Prepared emptyLayer(const QString &name, const QString &blendKey, int sec
|
||||
prepared.isDivider = section == 3;
|
||||
prepared.channels = emptyChannels();
|
||||
if (record.mask) {
|
||||
prepared.channels.push_back({-2, channelPayload(grayPlane(*record.mask), record.mask->width(), record.mask->height())});
|
||||
prepared.channels.push_back({-2, channelPayload(grayPlane(*record.mask), record.mask->width(), record.mask->height(), large)});
|
||||
prepared.maskBottom = record.mask->height();
|
||||
prepared.maskRight = record.mask->width();
|
||||
}
|
||||
return prepared;
|
||||
}
|
||||
|
||||
inline void additional(Buffer &buffer, const char *key, const QByteArray &payload)
|
||||
// Swift's AdditionalLayerInfo: one more block, before the name's.
|
||||
struct AdditionalLayerInfo {
|
||||
const char *key;
|
||||
QByteArray payload;
|
||||
};
|
||||
|
||||
inline void additional(Buffer &buffer, const char *key, const QByteArray &payload, bool large = false)
|
||||
{
|
||||
static const QStringList largeKeys{"LMsk", "Lr16", "Lr32", "Layr", "Mt16", "Mt32", "Mtrn", "Alph", "FMsk", "lnk2", "FEid", "FXid", "PxSD"};
|
||||
buffer.string("8BIM");
|
||||
buffer.string(key);
|
||||
buffer.u32(quint32(payload.size()));
|
||||
buffer.length(quint64(payload.size()), large && largeKeys.contains(QString::fromLatin1(key)));
|
||||
buffer.bytes(payload);
|
||||
if (payload.size() % 2 == 1)
|
||||
buffer.u8(0);
|
||||
@@ -189,7 +203,7 @@ inline QByteArray luni(const QString &name)
|
||||
return data.data;
|
||||
}
|
||||
|
||||
inline QByteArray extraData(const Prepared &item)
|
||||
inline QByteArray extraData(const Prepared &item, bool large, const std::optional<AdditionalLayerInfo> &info)
|
||||
{
|
||||
Buffer extra;
|
||||
if (item.record.mask && item.maskRight > item.maskLeft && item.maskBottom > item.maskTop) {
|
||||
@@ -209,18 +223,20 @@ inline QByteArray extraData(const Prepared &item)
|
||||
extra.u8(uchar(pascal.size()));
|
||||
extra.bytes(pascal);
|
||||
extra.bytes(QByteArray((4 - ((1 + pascal.size()) % 4)) % 4, '\0'));
|
||||
additional(extra, "luni", luni(item.record.name));
|
||||
if (info)
|
||||
additional(extra, info->key, info->payload, large);
|
||||
additional(extra, "luni", luni(item.record.name), large);
|
||||
if (item.record.isGroup || item.isDivider) {
|
||||
Buffer payload;
|
||||
payload.u32(item.isDivider ? 3 : 1);
|
||||
payload.string("8BIM");
|
||||
payload.bytes((item.isDivider ? QStringLiteral("norm") : item.record.blendKey + QStringLiteral(" ")).left(4).toLatin1());
|
||||
additional(extra, "lsct", payload.data);
|
||||
additional(extra, "lsct", payload.data, large);
|
||||
}
|
||||
return extra.data;
|
||||
}
|
||||
|
||||
inline void writeRecord(Buffer &buffer, const Prepared &item)
|
||||
inline void writeRecord(Buffer &buffer, const Prepared &item, bool large, const std::optional<AdditionalLayerInfo> &info)
|
||||
{
|
||||
buffer.i32(item.top);
|
||||
buffer.i32(item.left);
|
||||
@@ -229,7 +245,7 @@ inline void writeRecord(Buffer &buffer, const Prepared &item)
|
||||
buffer.u16(quint16(item.channels.size()));
|
||||
for (const Channel &channel : item.channels) {
|
||||
buffer.i16(channel.id);
|
||||
buffer.u32(quint32(channel.payload.size()));
|
||||
buffer.length(quint64(channel.payload.size()), large);
|
||||
}
|
||||
buffer.string("8BIM");
|
||||
buffer.bytes((item.record.blendKey + QStringLiteral(" ")).left(4).toLatin1());
|
||||
@@ -237,49 +253,50 @@ inline void writeRecord(Buffer &buffer, const Prepared &item)
|
||||
buffer.u8(item.record.clipping ? 1 : 0);
|
||||
buffer.u8(item.record.isVisible ? 0 : 2);
|
||||
buffer.u8(0);
|
||||
const QByteArray extra = extraData(item);
|
||||
const QByteArray extra = extraData(item, large, info);
|
||||
buffer.u32(quint32(extra.size()));
|
||||
buffer.bytes(extra);
|
||||
}
|
||||
|
||||
inline void emitLayers(const PSDDocument &document, std::optional<QUuid> parent, std::vector<Prepared> &prepared)
|
||||
inline void emitLayers(const PSDDocument &document, std::optional<QUuid> parent, std::vector<Prepared> &prepared, bool large)
|
||||
{
|
||||
// File order runs bottom to top: divider, children, folder.
|
||||
for (const PSDRecord &record : document.layers) {
|
||||
if (record.parentID != parent)
|
||||
continue;
|
||||
if (record.isGroup) {
|
||||
prepared.push_back(emptyLayer(QStringLiteral("</Layer group>"), QStringLiteral("norm"), 3, parent));
|
||||
emitLayers(document, record.id, prepared);
|
||||
prepared.push_back(emptyLayer(record.name, record.blendKey, 1, record.parentID, &record));
|
||||
prepared.push_back(emptyLayer(QStringLiteral("</Layer group>"), QStringLiteral("norm"), 3, parent, nullptr, large));
|
||||
emitLayers(document, record.id, prepared, large);
|
||||
prepared.push_back(emptyLayer(record.name, record.blendKey, 1, record.parentID, &record, large));
|
||||
} else {
|
||||
prepared.push_back(layer(record));
|
||||
prepared.push_back(layer(record, large));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline QByteArray layerSection(const PSDDocument &document)
|
||||
inline QByteArray layerSection(const PSDDocument &document, bool large, const std::optional<AdditionalLayerInfo> &info)
|
||||
{
|
||||
std::vector<Prepared> prepared;
|
||||
emitLayers(document, std::nullopt, prepared);
|
||||
emitLayers(document, std::nullopt, prepared, large);
|
||||
Buffer records, payloads;
|
||||
records.i16(qint16(prepared.size()));
|
||||
for (const Prepared &item : prepared) {
|
||||
writeRecord(records, item);
|
||||
writeRecord(records, item, large, info);
|
||||
for (const Channel &channel : item.channels)
|
||||
payloads.bytes(channel.payload);
|
||||
}
|
||||
Buffer info;
|
||||
info.u32(0);
|
||||
info.bytes(records.data);
|
||||
info.bytes(payloads.data);
|
||||
if (info.data.size() % 2 == 1)
|
||||
info.u8(0);
|
||||
Buffer section;
|
||||
section.length(0, large);
|
||||
section.bytes(records.data);
|
||||
section.bytes(payloads.data);
|
||||
if (section.data.size() % 2 == 1)
|
||||
section.u8(0);
|
||||
const int field = large ? 8 : 4;
|
||||
Buffer length;
|
||||
length.u32(quint32(info.data.size() - 4));
|
||||
info.data.replace(0, 4, length.data);
|
||||
info.u32(0);
|
||||
return info.data;
|
||||
length.length(quint64(section.data.size() - field), large);
|
||||
section.data.replace(0, field, length.data);
|
||||
section.u32(0);
|
||||
return section.data;
|
||||
}
|
||||
|
||||
inline QByteArray resolutionResource(double resolution)
|
||||
@@ -299,11 +316,11 @@ inline QByteArray resolutionResource(double resolution)
|
||||
return resource.data;
|
||||
}
|
||||
|
||||
inline QByteArray data(const PSDDocument &document, const QImage &composite)
|
||||
inline QByteArray data(const PSDDocument &document, const QImage &composite, bool large = false, const std::optional<AdditionalLayerInfo> &info = std::nullopt)
|
||||
{
|
||||
Buffer file;
|
||||
file.string("8BPS");
|
||||
file.u16(1);
|
||||
file.u16(large ? 2 : 1);
|
||||
file.bytes(QByteArray(6, '\0'));
|
||||
file.u16(4);
|
||||
file.u32(quint32(document.height));
|
||||
@@ -314,8 +331,8 @@ inline QByteArray data(const PSDDocument &document, const QImage &composite)
|
||||
const QByteArray resources = resolutionResource(document.resolution);
|
||||
file.u32(quint32(resources.size()));
|
||||
file.bytes(resources);
|
||||
const QByteArray layers = layerSection(document);
|
||||
file.u32(quint32(layers.size()));
|
||||
const QByteArray layers = layerSection(document, large, info);
|
||||
file.length(quint64(layers.size()), large);
|
||||
file.bytes(layers);
|
||||
// The composite, PackBits by channel.
|
||||
QImage flattened = BrushRaster::context(document.width, document.height, false);
|
||||
@@ -326,9 +343,10 @@ inline QByteArray data(const PSDDocument &document, const QImage &composite)
|
||||
file.u16(1);
|
||||
QByteArray counts, packed;
|
||||
for (const std::vector<uchar> *plane : {&split.red, &split.green, &split.blue, &split.alpha}) {
|
||||
const QByteArray encoded = encode(*plane, document.width, document.height);
|
||||
counts.append(encoded.left(document.height * 2));
|
||||
packed.append(encoded.mid(document.height * 2));
|
||||
const QByteArray encoded = encode(*plane, document.width, document.height, large);
|
||||
const int countBytes = document.height * (large ? 4 : 2);
|
||||
counts.append(encoded.left(countBytes));
|
||||
packed.append(encoded.mid(countBytes));
|
||||
}
|
||||
file.bytes(counts);
|
||||
file.bytes(packed);
|
||||
|
||||
@@ -86,8 +86,9 @@ void PSDReaderLayerTests::masksAndLayersSpendOneBudget()
|
||||
masked.channels.push_back({-2, QByteArray("\0\0\x10\x20\x30\x40", 6)});
|
||||
QCOMPARE(PSDReader::read(PSDReaderFixtures::file({masked}), 4).layers[0].mask.value().size(), QSize(2, 2));
|
||||
QCOMPARE(refusal<ImageImportError>([&] { PSDReader::read(PSDReaderFixtures::file({masked}), 3); }), std::optional(ImageImportError::Kind::tooLarge));
|
||||
// Past the side, then still past three pixels once cut.
|
||||
masked.mask = QRect(0, 0, 30'001, 1);
|
||||
QCOMPARE(refusal<ImageImportError>([&] { PSDReader::read(PSDReaderFixtures::file({masked})); }), std::optional(ImageImportError::Kind::tooLarge));
|
||||
QCOMPARE(refusal<ImageImportError>([&] { PSDReader::read(PSDReaderFixtures::file({masked}), 3); }), std::optional(ImageImportError::Kind::tooLarge));
|
||||
}
|
||||
|
||||
void PSDReaderLayerTests::resourcesEndWhereTheyDeclare()
|
||||
|
||||
@@ -192,7 +192,8 @@ void PSDRoundTripTests::oversizedLayerBoundsAreRejected()
|
||||
{
|
||||
const QByteArray raw("\0\0", 2);
|
||||
const auto oversized = layerFile(30'000, 30'000, {{-1, raw}, {0, raw}, {1, raw}, {2, raw}});
|
||||
QCOMPARE(refusal<ImageImportError>([&] { PSDReader::read(oversized); }), std::optional(ImageImportError::Kind::tooLarge));
|
||||
// Cut to its canvas it fits: fifty pixels refuse it.
|
||||
QCOMPARE(refusal<ImageImportError>([&] { PSDReader::read(oversized, 50); }), std::optional(ImageImportError::Kind::tooLarge));
|
||||
const QImage fill = PSDFixture::colorImage(20, 20, 1, 0, 0);
|
||||
const QByteArray data = PSDFixture::data(PSDDocument{20, 20, 72, {PSDFixture::record("Huge", fill, QRectF(0, 0, 20, 20))}}, fill);
|
||||
QCOMPARE(refusal<ImageImportError>([&] { PSDReader::read(data, 50); }), std::optional(ImageImportError::Kind::tooLarge));
|
||||
@@ -287,7 +288,7 @@ void PSDRoundTripTests::folderOpacityImportsOntoTheFolder()
|
||||
|
||||
void PSDRoundTripTests::unsupportedHeadersAreRejected()
|
||||
{
|
||||
QCOMPARE(refusal<PSDError>([] { PSDReader::read(header(2)); }), std::optional(PSDError::Kind::unsupportedVersion));
|
||||
QCOMPARE(refusal<PSDError>([] { PSDReader::read(header(3)); }), std::optional(PSDError::Kind::unsupportedVersion));
|
||||
QCOMPARE(refusal<PSDError>([] { PSDReader::read(header(1, 8, 8, 8, 4)); }), std::optional(PSDError::Kind::unsupportedColorMode));
|
||||
QCOMPARE(refusal<PSDError>([] { PSDReader::read(header(1, 8, 8, 16)); }), std::optional(PSDError::Kind::unsupportedDepth));
|
||||
QCOMPARE(refusal<ImageImportError>([] { PSDReader::read(header(1, 30'001, 10)); }), std::optional(ImageImportError::Kind::tooLarge));
|
||||
@@ -297,7 +298,7 @@ void PSDRoundTripTests::unsupportedHeadersAreRejected()
|
||||
QCOMPARE(refusal<PSDError>([] { PSDReader::read(header()); }), std::optional(PSDError::Kind::truncated));
|
||||
QCOMPARE(QString::fromUtf8(PSDError(PSDError::Kind::unsupportedDepth).what()), QString("Only 8-bit RGB Photoshop files can be imported."));
|
||||
QCOMPARE(QString::fromUtf8(PSDError(PSDError::Kind::unsupportedColorMode).what()), QString("Only 8-bit RGB Photoshop files can be imported."));
|
||||
QCOMPARE(QString::fromUtf8(PSDError(PSDError::Kind::unsupportedVersion).what()), QString("Large Document (.psb) Photoshop files aren’t supported."));
|
||||
QCOMPARE(QString::fromUtf8(PSDError(PSDError::Kind::unsupportedVersion).what()), QString("This Photoshop file uses a format version OmaPhoto can’t read."));
|
||||
QCOMPARE(QString::fromUtf8(PSDError(PSDError::Kind::truncated).what()), QString("The Photoshop file could not be read. It may be damaged or incomplete."));
|
||||
QCOMPARE(QString::fromUtf8(PSDError(PSDError::Kind::unsupportedCompression).what()),
|
||||
QString("This Photoshop file uses a layer compression method that isn’t supported."));
|
||||
|
||||
@@ -207,6 +207,8 @@ compositor_test(RemoveBackgroundTests)
|
||||
compositor_test(SelectSubjectTests)
|
||||
compositor_test(ObjectSelectionTests)
|
||||
compositor_test(PSDRoundTripTests)
|
||||
compositor_test(PSBImportTests)
|
||||
compositor_test(CropToCanvasImportTests)
|
||||
compositor_test(PSDVectorTests)
|
||||
compositor_test(PSDReaderTests)
|
||||
compositor_test(PSDReaderLayerTests)
|
||||
|
||||
Reference in new issue
Block a user