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
@@ -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"
|
||||
Reference in new issue
Block a user