381 lines
17 KiB
C++
381 lines
17 KiB
C++
#include "IO/PSD/PSDText.h"
|
|
#include "IO/PSD/PSDText+Descriptor.h"
|
|
#include "IO/PSD/PSDText+Engine.h"
|
|
#include "PSDFixture+Text.h"
|
|
#include <QtTest>
|
|
|
|
// The type block's two readers: engine dictionaries and descriptors.
|
|
namespace {
|
|
// A descriptor of these items, read back.
|
|
std::optional<PSDDescriptor::Items> described(const std::vector<std::pair<QByteArray, QByteArray>> &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<PSDTextEngine::Engine> 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 <FEFF00480069> /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<double> 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 <FEFF0048 00> >>").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<QByteArray, int> 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<std::pair<QByteArray, double>>{{"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<double>(items.at("C").value), -2.0);
|
|
QCOMPARE(std::get<double>(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<double>::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<std::pair<QByteArray, QByteArray>> 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<double>(std::get<PSDDescriptor::Items>(items.at("G").value).at("N").value), 7.0);
|
|
const auto &values = std::get<std::vector<PSDDescriptor::Value>>(items.at("L").value);
|
|
QCOMPARE(values.size(), size_t(2));
|
|
QCOMPARE(std::get<double>(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<std::pair<QByteArray, double>>{{"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 <feff006a> >>").value();
|
|
QCOMPARE(PSDTextEngine::string(PSDTextEngine::walk(&lower, {"X"})).value(), QString("j"));
|
|
}
|
|
|
|
void PSDTextReaderTests::loneSurrogatesReadAsReplacements()
|
|
{
|
|
const QString replaced = QString(QChar::ReplacementCharacter);
|
|
const std::vector<std::pair<QByteArray, QString>> 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 <FEFF" + units + "> >>").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<std::pair<QByteArray, QString>> 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"
|