#include "IO/PSD/PSDText.h" #include "IO/PSD/PSDText+Descriptor.h" #include "IO/PSD/PSDText+Engine.h" #include "PSDFixture+Text.h" #include // The type block's two readers: engine dictionaries and descriptors. namespace { // A descriptor of these items, read back. std::optional described(const std::vector> &items, quint32 version = 16) { PSDFixture::Buffer data; PSDFixture::descriptor(data, "Test", items); data.data.replace(0, 4, QByteArray("\0\0\0", 3) + char(version)); PSDDescriptor::Reader reader{data.data}; return reader.descriptor(true); } QByteArray unicode(const QByteArray &type, const QString &text) { return type + PSDFixture::textItem(text).mid(4); } std::optional engine(const char *text) { return PSDTextEngine::parse(QByteArray(text)); } } class PSDTextReaderTests : public QObject { Q_OBJECT private slots: void theEngineReadsItsPostScriptForms(); void deepNestingReadsAsBrokenWithoutACrash(); void theDescriptorReadsEveryType(); void theDescriptorReadsNestingAndNames(); void loneSurrogatesReadAsReplacements(); void listsAndReferencesStopAtTenThousand(); void coloursReadInEachForm(); void theSourcesKeepSwiftsPrecedence(); void theStyleRoundsAndScalesAsSwift(); void versionsAndLimitsHoldAtTheirEdges(); }; void PSDTextReaderTests::theEngineReadsItsPostScriptForms() { using namespace PSDTextEngine; const Engine read = engine("junk << /A /B (a\\101\\n\\(\\\nz) /C [1 -2.5 .5 +3 1e2 1.] % note\n" " /D true /E null /F /Name /G <41 4 2> /A2 << /K false >> >>") .value(); QCOMPARE(string(walk(&read, {"A"})).value(), QString("Hi")); QCOMPARE(string(walk(&read, {"B"})).value(), QString("aA\n(z")); const Array numbers = array(walk(&read, {"C"})); QCOMPARE(numbers.size(), size_t(6)); const std::vector expected{1, -2.5, 0.5, 3, 100, 1}; for (size_t index = 0; index < expected.size(); ++index) QCOMPARE(number(&numbers[index]).value(), expected[index]); QCOMPARE(boolean(walk(&read, {"D"})).value(), true); QCOMPARE(string(walk(&read, {"E"})).value(), QString()); QCOMPARE(string(walk(&read, {"F"})).value(), QString("Name")); QCOMPARE(string(walk(&read, {"G"})).value(), QString("AB")); QCOMPARE(boolean(walk(&read, {"A2", "K"})).value(), false); QVERIFY(!walk(&read, {"A", "B"})); // Overflow reads as infinity, as Swift's `Double` reads it. const Engine huge = engine("<< /X 1e999 >>").value(); QVERIFY(std::isinf(number(walk(&huge, {"X"})).value())); // Broken forms fail the whole dictionary. for (const char *broken : {"<< /X - >>", "<< /X 1e >>", "<< /X [1 >>", "<< X 1 >>", "<< /X (a\\", "[1 2]", "<< /X 1", "<< /X <41 >>"}) QVERIFY2(!engine(broken), broken); // Three octal digits; `)` ends names; words stand alone. const Engine octal = engine("<< /X (\\1011) >>").value(); QCOMPARE(string(walk(&octal, {"X"})).value(), QString("A1")); QVERIFY(!engine("<< /A [/B)] >>")); QVERIFY(!engine("<< /X [true5] >>")); // A name with a high byte reads empty. const Engine high = engine("<< /\xC3\xA9 1 >>").value(); QCOMPARE(number(walk(&high, {""})).value(), 1.0); // An odd UTF-16 string reads empty. const Engine odd = engine("<< /X >>").value(); QCOMPARE(string(walk(&odd, {"X"})).value(), QString()); } void PSDTextReaderTests::deepNestingReadsAsBrokenWithoutACrash() { const QByteArray deep = "<< /X " + QByteArray(200'000, '[') + QByteArray(200'000, ']') + " >>"; QVERIFY(!PSDTextEngine::parse(deep)); const QByteArray fits = "<< /X " + QByteArray(500, '[') + QByteArray(500, ']') + " >>"; QVERIFY(PSDTextEngine::parse(fits)); // A descriptor of lists within lists, far past the cap. PSDFixture::Buffer nested; nested.u32(16); nested.u32(0); PSDFixture::id(nested, "TxLr"); nested.u32(1); PSDFixture::id(nested, "Deep"); for (int depth = 0; depth < 100'000; ++depth) { nested.string("VlLs"); nested.u32(1); } nested.string("long"); nested.i32(1); PSDDescriptor::Reader reader{nested.data}; QVERIFY(!reader.descriptor(true)); } void PSDTextReaderTests::theDescriptorReadsEveryType() { using PSDFixture::Buffer; Buffer comp, flag, alias, reference, global, type; comp.string("comp"); comp.u64(quint64(-2)); flag.string("bool"); flag.u8(1); alias.string("alis"); alias.u32(3); alias.string("abc"); reference.string("obj "); reference.u32(7); for (const char *form : {"prop", "Clss", "Enmr", "rele", "Idnt", "indx", "name"}) { reference.string(form); const QByteArray name = PSDFixture::textItem(QStringLiteral("n")).mid(4); const std::map identifiers{{"prop", 2}, {"Clss", 1}, {"Enmr", 3}, {"rele", 1}}; if (QByteArray(form) != "Idnt" && QByteArray(form) != "indx") reference.bytes(name); for (int count = 0; count < (identifiers.contains(form) ? identifiers.at(form) : 0); ++count) PSDFixture::id(reference, "Idnt"); if (QByteArray(form) == "rele" || QByteArray(form) == "Idnt" || QByteArray(form) == "indx") reference.i32(4); } global.bytes(unicode("GlbC", QStringLiteral("g"))); PSDFixture::id(global, "Clss"); type.bytes(unicode("type", QStringLiteral("t"))); PSDFixture::id(type, "Clss"); Buffer box; box.string("Objc"); box.u32(0); PSDFixture::id(box, "Rctn"); box.u32(4); for (const auto &[key, value] : std::vector>{{"Left", 1}, {"Top", 2}, {"Rght", 5}, {"Btom", 9}}) { PSDFixture::id(box, key); box.string("doub"); PSDFixture::f64(box, value); } const PSDDescriptor::Items items = described({{"C", comp.data}, {"B", flag.data}, {"A", alias.data}, {"R", reference.data}, {"G", global.data}, {"T", type.data}, {"box", box.data}, {"Txt ", PSDFixture::textItem(QStringLiteral("after"))}}) .value(); QCOMPARE(std::get(items.at("C").value), -2.0); QCOMPARE(std::get(items.at("B").value), 0.0); QCOMPARE(PSDDescriptor::string(items, QStringLiteral("Txt ")).value(), QString("after")); // `Top` without its space still reads, as Swift trims. QCOMPARE(PSDDescriptor::rect(items, QStringLiteral("box")).value(), QRectF(1, 2, 4, 7)); // An infinite side is no rectangle. Buffer infinite = box; infinite.data.chop(8); PSDFixture::f64(infinite, std::numeric_limits::infinity()); QVERIFY(!PSDDescriptor::rect(described({{"box", infinite.data}}).value(), QStringLiteral("box"))); // Refusals: a version, a high byte, and each cap. QVERIFY(!described({{"Txt ", PSDFixture::textItem(QStringLiteral("x"))}}, 15)); QVERIFY(!described({{"K\x80", PSDFixture::textItem(QStringLiteral("x"))}})); Buffer data; data.string("tdta"); data.u32(8'000'001); data.bytes(QByteArray(8'000'001, 'x')); QVERIFY(!described({{"D", data.data}})); Buffer longAlias; longAlias.string("alis"); longAlias.u32(8'000'001); longAlias.bytes(QByteArray(8'000'001, 'x')); QVERIFY(!described({{"A", longAlias.data}})); Buffer words; words.string("TEXT"); words.u32(1'000'001); words.bytes(QByteArray(2'000'002, 'x')); QVERIFY(!described({{"W", words.data}})); std::vector> many; for (int index = 0; index <= 10'000; ++index) many.emplace_back(QByteArray::number(index), QByteArray("long\0\0\0\1", 8)); QVERIFY(!described(many)); many.pop_back(); QCOMPARE(described(many).value().size(), size_t(10'000)); } void PSDTextReaderTests::theDescriptorReadsNestingAndNames() { using PSDFixture::Buffer; Buffer global, list; global.string("GlbO"); global.u32(0); PSDFixture::id(global, "Innr"); global.u32(1); PSDFixture::id(global, "N"); global.bytes(QByteArray("long\0\0\0\7", 8)); list.string("VlLs"); list.u32(2); list.bytes(QByteArray("long\0\0\0\1", 8)); list.bytes(QByteArray("long\0\0\0\2", 8)); const PSDDescriptor::Items items = described({{"G", global.data}, {"L", list.data}}).value(); QCOMPARE(std::get(std::get(items.at("G").value).at("N").value), 7.0); const auto &values = std::get>(items.at("L").value); QCOMPARE(values.size(), size_t(2)); QCOMPARE(std::get(values[1].value), 2.0); // A key of 10,000 bytes reads; one more refuses. QVERIFY(described({{QByteArray(10'000, 'k'), QByteArray("long\0\0\0\1", 8)}})); QVERIFY(!described({{QByteArray(10'001, 'k'), QByteArray("long\0\0\0\1", 8)}})); // The exact side's key decides, number or not. Buffer box; box.string("Objc"); box.u32(0); PSDFixture::id(box, "Rctn"); box.u32(5); for (const auto &[key, value] : std::vector>{{"Left", 0}, {"Top", 0}, {"Rght", 200}, {"Btom", 80}}) { PSDFixture::id(box, key); box.string("doub"); PSDFixture::f64(box, value); } PSDFixture::id(box, "Top "); box.bytes(PSDFixture::textItem(QStringLiteral("bad"))); QVERIFY(!PSDDescriptor::rect(described({{"box", box.data}}).value(), QStringLiteral("box"))); // Lowercase hex reads as uppercase. const PSDTextEngine::Engine lower = engine("<< /X >>").value(); QCOMPARE(PSDTextEngine::string(PSDTextEngine::walk(&lower, {"X"})).value(), QString("j")); } void PSDTextReaderTests::loneSurrogatesReadAsReplacements() { const QString replaced = QString(QChar::ReplacementCharacter); const std::vector> cases{{"0048D800", "H" + replaced}, {"D800", replaced}, {"DC00", replaced}, {"0048D8000049", "H" + replaced + "I"}, {"D800DC00", QString::fromUtf16(u"\xD800\xDC00")}}; for (const auto &[units, expected] : cases) { const PSDTextEngine::Engine read = engine(("<< /X >>").constData()).value(); QCOMPARE(PSDTextEngine::string(PSDTextEngine::walk(&read, {"X"})).value(), expected); PSDFixture::Buffer text; text.string("TEXT"); text.u32(quint32(units.size() / 4)); text.bytes(QByteArray::fromHex(units)); QCOMPARE(PSDDescriptor::string(described({{"Txt ", text.data}}).value(), QStringLiteral("Txt ")).value(), expected); } } void PSDTextReaderTests::listsAndReferencesStopAtTenThousand() { for (const quint32 count : {10'000u, 10'001u}) { PSDFixture::Buffer list, reference; list.string("VlLs"); list.u32(count); reference.string("obj "); reference.u32(count); for (quint32 index = 0; index < count; ++index) { list.bytes(QByteArray("long\0\0\0\1", 8)); reference.string("Idnt"); reference.i32(1); } QCOMPARE(described({{"L", list.data}}).has_value(), count == 10'000u); QCOMPARE(described({{"R", reference.data}}).has_value(), count == 10'000u); } } void PSDTextReaderTests::coloursReadInEachForm() { using PSDFixture::TypeBlock; const auto colour = [](const QByteArray &values) { TypeBlock block; block.text = QStringLiteral("Hello"); block.values = values; const LayerTextStyle style = PSDText::parse(PSDFixture::typeExtra(block)).value().style; return std::array{style.red, style.green, style.blue}; }; QCOMPARE(colour("1.0 0 0 1"), (std::array{0.0, 0.0, 1.0})); QCOMPARE(colour("0.2 0.4 0.6"), (std::array{0.2, 0.4, 0.6})); QCOMPARE(colour("0.6"), (std::array{0.6, 0.6, 0.6})); // A red or green difference alone is a second style. for (const char *values : {"[ 1.0 1 0 0 ]", "[ 1.0 0 1 0 ]"}) { TypeBlock block; block.text = QStringLiteral("Hello"); block.secondOverride = QByteArray("/FillColor << /Values ") + values + " >>\n"; QCOMPARE(PSDText::parse(PSDFixture::typeExtra(block)).value().notes, std::vector{PSDText::firstStyleNote}); } } void PSDTextReaderTests::theSourcesKeepSwiftsPrecedence() { using PSDFixture::TypeBlock; // Present but empty words refuse; absent ones read the engine. TypeBlock empty; empty.engineText = QStringLiteral("Fallback"); QVERIFY(!PSDText::parse(PSDFixture::typeExtra(empty))); empty.typed = false; QCOMPARE(PSDText::parse(PSDFixture::typeExtra(empty)).value().style.content, QString("Fallback")); // `TySh` wins over `tySh`, broken or not. TypeBlock upper, lower; upper.text = QStringLiteral("Upper"); lower.text = QStringLiteral("Lower"); QVERIFY(!PSDText::parse({{QStringLiteral("TySh"), QByteArray("\0\1", 2)}, {QStringLiteral("tySh"), PSDFixture::tySh(lower)}})); const PSDText::Source both = PSDText::parse({{QStringLiteral("TySh"), PSDFixture::tySh(upper)}, {QStringLiteral("tySh"), PSDFixture::tySh(lower)}}).value(); QCOMPARE(both.style.content, QString("Upper")); } void PSDTextReaderTests::theStyleRoundsAndScalesAsSwift() { using PSDFixture::TypeBlock; const auto parsed = [](const TypeBlock &block) { return PSDText::parse(PSDFixture::typeExtra(block)).value(); }; // Manual leading scales with the matrix, as the size does. TypeBlock scaled; scaled.text = QStringLiteral("H\nH"); scaled.leading = 30; scaled.xx = scaled.yy = 2; QCOMPARE(parsed(scaled).style.fontSize, 48.0); QCOMPARE(parsed(scaled).style.leading, 60.0); // The font index and justification round half away from zero. TypeBlock faces; faces.text = QStringLiteral("Hello"); faces.font = QStringLiteral("DejaVuSans"); faces.secondFont = QStringLiteral("DejaVuSerif"); const std::vector> indices{{"1", "DejaVuSerif"}, {"0.6", "DejaVuSerif"}, {"0.4", "DejaVuSans"}, {"-0.6", "Helvetica"}}; for (const auto &[index, name] : indices) { faces.fontIndex = index; QCOMPARE(parsed(faces).style.fontName, name); } faces.justification = "1.6"; QVERIFY(parsed(faces).style.alignment == TextAlignment::center); QVERIFY(parsed(faces).notes.empty()); faces.justification = "2.5"; QVERIFY(parsed(faces).style.alignment == TextAlignment::left); QCOMPARE(parsed(faces).notes, std::vector{PSDText::justifyNote}); faces.justification = "0.4"; QVERIFY(parsed(faces).notes.empty()); // `Txt` without its space holds the words over the engine's. TypeBlock alias; alias.text = QStringLiteral("Alias words"); alias.wordsKey = "Txt"; alias.engineText = QStringLiteral("Fallback"); QCOMPARE(parsed(alias).style.content, QString("Alias words")); alias.withEngine = false; QCOMPARE(parsed(alias).style.content, QString("Alias words")); // A negative exponent keeps the engine and its style. TypeBlock tracked; tracked.text = QStringLiteral("Hello"); tracked.tracking = "1e-1"; tracked.red = 1; QCOMPARE(parsed(tracked).style.tracking, 0.0024); QCOMPARE(parsed(tracked).style.red, 1.0); const PSDTextEngine::Engine small = engine("<< /X -2.5E-2 >>").value(); QCOMPARE(PSDTextEngine::number(PSDTextEngine::walk(&small, {"X"})).value(), -0.025); } void PSDTextReaderTests::versionsAndLimitsHoldAtTheirEdges() { using PSDFixture::TypeBlock; TypeBlock block; block.text = QStringLiteral("Hello"); const QByteArray whole = PSDFixture::tySh(block); QVERIFY(PSDText::parse({{QStringLiteral("TySh"), whole}})); // Object version 1 and text version 50, whole blocks otherwise. QByteArray object = whole, text = whole; object[1] = 2; text[51] = 51; QVERIFY(!PSDText::parse({{QStringLiteral("TySh"), object}})); QVERIFY(!PSDText::parse({{QStringLiteral("TySh"), text}})); // 100,000 UTF-16 units at most. block.text = QString(100'000, u'x'); QCOMPARE(PSDText::parse(PSDFixture::typeExtra(block)).value().style.content.size(), qsizetype(100'000)); block.text += u'x'; QVERIFY(!PSDText::parse(PSDFixture::typeExtra(block))); // 511 lists round a number read; 512 refuse. for (const int depth : {511, 512}) { PSDFixture::Buffer lists; for (int level = 0; level < depth; ++level) { lists.string("VlLs"); lists.u32(1); } lists.bytes(QByteArray("long\0\0\0\1", 8)); QCOMPARE(described({{"Deep", lists.data}}).has_value(), depth == 511); } } QTEST_GUILESS_MAIN(PSDTextReaderTests) #include "PSDTextReaderTests.moc"