Import simple Photoshop text as editable text (12.11b2)

This commit is contained in:
雷电芽衣 committed 2026-09-29 09:04:50 -04:00
1 parent a2241601ec
commit 008493b0a3
20 files changed
+2081 -17

No files matched your search

+1 -1
View File
@@ -91,7 +91,7 @@ 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)});
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"));
}
+182
View File
@@ -0,0 +1,182 @@
#pragma once
#include "PSDFixture.h"
#include <bit>
// Swift's `PSDFixture.tySh`: a Photoshop 6 type block.
namespace PSDFixture {
struct TypeBlock {
QString text;
QString font = QStringLiteral("Helvetica");
double fontSize = 24;
double red = 0, green = 0, blue = 0;
QByteArray justification = "0";
QByteArray fontIndex = "0";
// Without it the words come from the engine alone.
bool typed = true;
// Swift's other forms: bare runs, no engine at all.
bool sheet = true;
bool withEngine = true;
QByteArray tracking = "0";
std::optional<double> leading;
bool fauxBold = false, fauxItalic = false, vertical = false, warp = false;
std::optional<double> secondSize, secondLeading, secondHorizontalScale, secondVerticalScale;
// Keys the second run's sheet repeats, later winning.
QByteArray secondOverride;
// The fill's values as written, else ARGB from the channels.
std::optional<QByteArray> values;
// The engine's own words, else `text`.
std::optional<QString> engineText;
// A second face in the set; the words' own key.
std::optional<QString> secondFont;
QByteArray wordsKey = "Txt ";
double tx = 40, ty = 50;
double xx = 1, xy = 0, yx = 0, yy = 1;
std::optional<std::array<double, 4>> bounds, glyphBounds;
};
inline void f64(Buffer &buffer, double value)
{
const quint64 bits = std::bit_cast<quint64>(value);
buffer.u32(quint32(bits >> 32));
buffer.u32(quint32(bits));
}
// A four-letter key takes a zero length.
inline void id(Buffer &buffer, const QByteArray &value)
{
buffer.u32(value.size() == 4 ? 0 : quint32(value.size()));
buffer.bytes(value);
}
inline void descriptor(Buffer &buffer, const QByteArray &classID, const std::vector<std::pair<QByteArray, QByteArray>> &items)
{
buffer.u32(16);
buffer.u32(0);
id(buffer, classID);
buffer.u32(quint32(items.size()));
for (const auto &[key, value] : items) {
id(buffer, key);
buffer.bytes(value);
}
}
inline QByteArray textItem(const QString &text)
{
Buffer item;
item.string("TEXT");
item.u32(quint32(text.size()));
for (const QChar unit : text)
item.u16(unit.unicode());
return item.data;
}
inline QByteArray enumItem(const QByteArray &type, const QByteArray &value)
{
Buffer item;
item.string("enum");
id(item, type);
id(item, value);
return item.data;
}
inline QByteArray rawItem(const QByteArray &payload)
{
Buffer item;
item.string("tdta");
item.u32(quint32(payload.size()));
item.bytes(payload);
return item.data;
}
inline QByteArray rectItem(const std::array<double, 4> &box)
{
Buffer item;
item.string("Objc");
item.u32(0);
id(item, "Rctn");
item.u32(4);
const std::array<QByteArray, 4> keys{"Left", "Top ", "Rght", "Btom"};
for (size_t index = 0; index < keys.size(); ++index) {
id(item, keys[index]);
item.string("UntF");
item.string("#Pnt");
f64(item, box[index]);
}
return item.data;
}
// Swift's `parenthesized`: backslashes before the three specials.
inline QByteArray parenthesized(const QString &text)
{
QByteArray encoded("(");
for (const char byte : text.toUtf8()) {
if (byte == '\\' || byte == '(' || byte == ')')
encoded.append('\\');
encoded.append(byte);
}
return encoded + ')';
}
inline QByteArray number(double value)
{
return QByteArray::number(value, 'g', 17);
}
inline QByteArray flag(bool value)
{
return value ? "true" : "false";
}
inline QByteArray run(const TypeBlock &block, double size, std::optional<double> leading, double horizontal, double vertical,
const QByteArray &override = {})
{
const QByteArray open = block.sheet ? "<<\n/StyleSheet\n<<\n/StyleSheetData\n<<\n" : "<<\n";
const QByteArray close = block.sheet ? " ]\n>>\n>>\n>>\n>>" : " ]\n>>\n>>";
return open + "/Font " + block.fontIndex + "\n/FontSize " + number(size) + "\n/FauxBold " + flag(block.fauxBold)
+ "\n/FauxItalic " + flag(block.fauxItalic) + "\n/AutoLeading " + flag(!leading) + "\n/Leading " + number(leading.value_or(size * 1.2))
+ "\n/Tracking " + block.tracking + "\n/HorizontalScale " + number(horizontal) + "\n/VerticalScale " + number(vertical)
+ "\n/FillColor\n<<\n/Type 1\n/Values [ "
+ block.values.value_or("1.0 " + number(block.red) + " " + number(block.green) + " " + number(block.blue))
+ " ]\n>>\n" + override + close.mid(6);
}
inline QByteArray engine(const TypeBlock &block)
{
QByteArray runs = run(block, block.fontSize, block.leading, 1, 1);
if (block.secondSize || block.secondLeading || block.secondHorizontalScale || block.secondVerticalScale || !block.secondOverride.isEmpty())
runs += "\n" + run(block, block.secondSize.value_or(block.fontSize), block.secondLeading ? block.secondLeading : block.leading,
block.secondHorizontalScale.value_or(1), block.secondVerticalScale.value_or(1), block.secondOverride);
return "<<\n/EngineDict\n<<\n/Editor\n<<\n/Text " + parenthesized(block.engineText.value_or(block.text)) + "\n>>\n/ParagraphRun\n<<\n/RunArray\n[\n<<\n"
+ "/ParagraphSheet\n<<\n/Properties\n<<\n/Justification " + block.justification + "\n>>\n>>\n>>\n]\n>>\n"
+ "/StyleRun\n<<\n/RunArray\n[\n" + runs + "\n]\n>>\n>>\n/ResourceDict\n<<\n/FontSet\n[\n<<\n/Name " + parenthesized(block.font)
+ "\n>>\n" + (block.secondFont ? "<<\n/Name " + parenthesized(*block.secondFont) + "\n>>\n" : QByteArray()) + "]\n>>\n>>";
}
inline QByteArray tySh(const TypeBlock &block)
{
Buffer data;
data.u16(1);
for (const double value : {block.xx, block.xy, block.yx, block.yy, block.tx, block.ty})
f64(data, value);
data.u16(50);
std::vector<std::pair<QByteArray, QByteArray>> items;
if (block.typed)
items.emplace_back(block.wordsKey, textItem(block.text));
items.emplace_back("Ornt", enumItem("Ornt", block.vertical ? "Vrtc" : "Hrzn"));
if (block.bounds)
items.emplace_back("bounds", rectItem(*block.bounds));
if (block.glyphBounds)
items.emplace_back("boundingBox", rectItem(*block.glyphBounds));
if (block.withEngine)
items.emplace_back("EngineData", rawItem(engine(block)));
descriptor(data, "TxLr", items);
data.u16(1);
descriptor(data, "warp", {{"warpStyle", enumItem("warpStyle", block.warp ? "warpArc" : "warpNone")}});
return data.data;
}
inline std::map<QString, QByteArray> typeExtra(const TypeBlock &block)
{
return {{QStringLiteral("TySh"), tySh(block)}};
}
}
+6 -6
View File
@@ -203,7 +203,7 @@ inline QByteArray luni(const QString &name)
return data.data;
}
inline QByteArray extraData(const Prepared &item, bool large, const std::optional<AdditionalLayerInfo> &info)
inline QByteArray extraData(const Prepared &item, bool large, const std::vector<AdditionalLayerInfo> &info)
{
Buffer extra;
if (item.record.mask && item.maskRight > item.maskLeft && item.maskBottom > item.maskTop) {
@@ -223,8 +223,8 @@ inline QByteArray extraData(const Prepared &item, bool large, const std::optiona
extra.u8(uchar(pascal.size()));
extra.bytes(pascal);
extra.bytes(QByteArray((4 - ((1 + pascal.size()) % 4)) % 4, '\0'));
if (info)
additional(extra, info->key, info->payload, large);
for (const AdditionalLayerInfo &block : info)
additional(extra, block.key, block.payload, large);
additional(extra, "luni", luni(item.record.name), large);
if (item.record.isGroup || item.isDivider) {
Buffer payload;
@@ -236,7 +236,7 @@ inline QByteArray extraData(const Prepared &item, bool large, const std::optiona
return extra.data;
}
inline void writeRecord(Buffer &buffer, const Prepared &item, bool large, const std::optional<AdditionalLayerInfo> &info)
inline void writeRecord(Buffer &buffer, const Prepared &item, bool large, const std::vector<AdditionalLayerInfo> &info)
{
buffer.i32(item.top);
buffer.i32(item.left);
@@ -274,7 +274,7 @@ inline void emitLayers(const PSDDocument &document, std::optional<QUuid> parent,
}
}
inline QByteArray layerSection(const PSDDocument &document, bool large, const std::optional<AdditionalLayerInfo> &info)
inline QByteArray layerSection(const PSDDocument &document, bool large, const std::vector<AdditionalLayerInfo> &info)
{
std::vector<Prepared> prepared;
emitLayers(document, std::nullopt, prepared, large);
@@ -316,7 +316,7 @@ inline QByteArray resolutionResource(double resolution)
return resource.data;
}
inline QByteArray data(const PSDDocument &document, const QImage &composite, bool large = false, const std::optional<AdditionalLayerInfo> &info = std::nullopt)
inline QByteArray data(const PSDDocument &document, const QImage &composite, bool large = false, const std::vector<AdditionalLayerInfo> &info = {})
{
Buffer file;
file.string("8BPS");
+380
View File
@@ -0,0 +1,380 @@
#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"
+468
View File
@@ -0,0 +1,468 @@
#include "IO/PSD/PSDDocumentBuilder.h"
#include "IO/PSD/PSDReader.h"
#include "IO/PSD/PSDVector.h"
#include "PSDFixture+Text.h"
#include "PSDVectorBuilders.h"
#include "PSDVectorFixtures.h"
#include <QtTest>
// Swift's PSDRoundTripTests, the text half, and the twin's own.
namespace {
using PSDFixture::TypeBlock;
std::optional<PSDText::Source> parsed(const TypeBlock &block)
{
return PSDText::parse(PSDFixture::typeExtra(block));
}
TypeBlock hello()
{
TypeBlock block;
block.text = QStringLiteral("Hello");
return block;
}
bool notes(const std::vector<QString> &list, const QString &note)
{
return std::find(list.begin(), list.end(), note) != list.end();
}
bool reported(const PSDImport &imported, const QString &message)
{
return std::any_of(imported.conversions.begin(), imported.conversions.end(), [&](const PSDConversion &item) { return item.message == message; });
}
// One layer carrying the block, read back through a file.
PSDImport importedFile(const QString &name, const QImage &pixels, int side, const TypeBlock &block)
{
const PSDRecord record = PSDFixture::record(name, pixels, QRectF(QPointF(), pixels.size()));
const QByteArray file = PSDFixture::data(PSDDocument{side, side, 72, {record}}, PSDFixture::colorImage(side, side, 1, 1, 1), false,
{PSDFixture::AdditionalLayerInfo{"TySh", PSDFixture::tySh(block)}});
return PSDDocumentBuilder::makeImport(PSDReader::read(file));
}
}
class PSDTextTests : public QObject {
Q_OBJECT
private slots:
void photoshopPointTextImportsAsEditableText();
void photoshopTextSizeUsesMatrixScaleNotDocumentResolution();
void photoshopTextKeepsTheFirstStyleAndReportsTheRest();
void photoshopTextReportsLeadingOnlyStyleDifferences();
void photoshopParagraphTextKeepsItsBox();
void oversizedPhotoshopParagraphFrameStaysPixels();
void warpedPhotoshopTextStaysEditableAndSaysSo();
void verticalOrBrokenPhotoshopTextStaysPixels();
void missingPhotoshopFontIsReportedButStaysEditable();
void rotatedPhotoshopTextKeepsItsAngle();
void wildNumbersNeverTrap();
void theWordsAreCleanedAndMayComeFromTheEngine();
void framesReadStandardisedAndPlaced();
void pointTextSitsOnItsBaseline();
void textThatCannotBeDrawnStaysPixels();
void theStyleReadsAsSwiftReadsIt();
void typeWithAShapeStaysType();
};
void PSDTextTests::photoshopPointTextImportsAsEditableText()
{
TypeBlock block = hello();
const PSDText::Source source = parsed(block).value();
QCOMPARE(source.style.content, QString("Hello"));
QCOMPARE(source.style.fontName, QString("Helvetica"));
QCOMPARE(source.style.fontSize, 24.0);
QVERIFY(source.style.red == 0 && source.style.green == 0 && source.style.blue == 0);
QVERIFY(source.style.alignment == TextAlignment::left);
QVERIFY(source.notes.empty());
const PSDRecord record = PSDFixture::record(QStringLiteral("Greeting"), PSDFixture::colorImage(8, 8, 0, 0, 0), QRectF(1, 2, 8, 8));
const PSDDocument document = PSDReader::read(PSDFixture::data(PSDDocument{32, 32, 72, {record}}, PSDFixture::colorImage(32, 32, 1, 1, 1), false,
{PSDFixture::AdditionalLayerInfo{"TySh", PSDFixture::tySh(block)}}));
QVERIFY(document.layers.at(0).kind == PSDLayerKind::text);
QCOMPARE(document.layers.at(0).text.value().style.content, QString("Hello"));
const PSDImport imported = PSDDocumentBuilder::makeImport(document);
const ImageLayer &layer = imported.layers.at(0);
const LayerText live = layer.liveText().value();
QCOMPARE(live.style.content, QString("Hello"));
QCOMPARE(live.style.fontName, QString("Helvetica"));
QCOMPARE(live.style.fontSize, 24.0);
QVERIFY(layer.asset.value().identity() == live.image);
QVERIFY(!reported(imported, PSDText::rasterizedNote));
QVERIFY(std::abs(layer.transform.origin.x() - 40) < 80);
QVERIFY(std::abs(layer.transform.origin.y() - 50) < 80);
// The rendered words replace Photoshop's cached pixels.
const PSDText::Rendered rendered = PSDText::render(document.layers.at(0).text.value());
QCOMPARE(layer.transform, rendered.transform);
QCOMPARE(layer.asset.value().image(), rendered.image);
QVERIFY(rendered.image.size() != QSize(8, 8));
}
void PSDTextTests::photoshopTextSizeUsesMatrixScaleNotDocumentResolution()
{
// Only the matrix scales the engine's size.
QCOMPARE(parsed(hello()).value().style.fontSize, 24.0);
TypeBlock doubled = hello();
doubled.xx = doubled.yy = 2;
QCOMPARE(parsed(doubled).value().style.fontSize, 48.0);
doubled.fontSize = 25;
QCOMPARE(parsed(doubled).value().style.fontSize, 50.0);
}
void PSDTextTests::photoshopTextKeepsTheFirstStyleAndReportsTheRest()
{
TypeBlock block = hello();
block.red = 1;
block.justification = "2";
block.tracking = "1000";
block.leading = 30;
block.secondSize = 48;
const PSDText::Source source = parsed(block).value();
QCOMPARE(source.style.content, QString("Hello"));
QVERIFY(source.style.alignment == TextAlignment::center);
QCOMPARE(source.style.tracking, 24.0);
QCOMPARE(source.style.leading, 30.0);
QCOMPARE(source.style.red, 1.0);
QCOMPARE(source.style.fontSize, 24.0);
QVERIFY(notes(source.notes, PSDText::firstStyleNote));
}
void PSDTextTests::photoshopTextReportsLeadingOnlyStyleDifferences()
{
TypeBlock byLeading = hello();
byLeading.leading = 30;
byLeading.secondLeading = 48;
const PSDText::Source leading = parsed(byLeading).value();
QCOMPARE(leading.style.leading, 30.0);
QVERIFY(notes(leading.notes, PSDText::firstStyleNote));
TypeBlock byScale = hello();
byScale.secondHorizontalScale = 1.2;
QVERIFY(notes(parsed(byScale).value().notes, PSDText::firstStyleNote));
// The same second run says nothing.
TypeBlock same = hello();
same.secondSize = 24;
QVERIFY(parsed(same).value().notes.empty());
// Each other field of the signature counts alone.
for (const char *field : {"/Font 1\n", "/Tracking 5\n", "/AutoLeading false\n", "/VerticalScale 1.2\n", "/FauxBold true\n",
"/FauxItalic true\n", "/FillColor << /Values [ 1.0 0 0 1 ] >>\n"}) {
TypeBlock second = hello();
second.secondOverride = field;
QVERIFY2(parsed(second).value().notes == std::vector{PSDText::firstStyleNote}, field);
}
}
void PSDTextTests::photoshopParagraphTextKeepsItsBox()
{
TypeBlock block = hello();
block.tx = 10;
block.ty = 30;
block.bounds = std::array{0.0, 0.0, 200.0, 80.0};
block.glyphBounds = std::array{0.0, -10.0, 40.0, 10.0};
const PSDText::Source source = parsed(block).value();
QVERIFY(source.anchorIsFrame);
QCOMPARE(source.style.boxSize.value(), QSizeF(224, 104));
QCOMPARE(source.documentAnchor, QPointF(10, 30));
// A frame barely wider than its letters stays point text.
block.bounds = std::array{0.0, 0.0, 44.0, 80.0};
QVERIFY(!parsed(block).value().anchorIsFrame);
QVERIFY(!parsed(block).value().style.boxSize);
}
void PSDTextTests::oversizedPhotoshopParagraphFrameStaysPixels()
{
TypeBlock block = hello();
block.bounds = std::array{0.0, 0.0, 40'000.0, 100.0};
block.glyphBounds = std::array{0.0, 0.0, 40.0, 10.0};
QVERIFY(!parsed(block));
const PSDImport imported = importedFile(QStringLiteral("Billboard"), PSDFixture::colorImage(4, 4, 0, 1, 0), 16, block);
const ImageLayer &layer = imported.layers.at(0);
QVERIFY(!layer.liveText());
QCOMPARE(layer.asset.value().size(), QSize(4, 4));
QVERIFY(reported(imported, PSDText::rasterizedNote));
}
void PSDTextTests::warpedPhotoshopTextStaysEditableAndSaysSo()
{
TypeBlock block = hello();
block.fauxBold = true;
block.warp = true;
const PSDText::Source source = parsed(block).value();
QCOMPARE(source.style.content, QString("Hello"));
QVERIFY(notes(source.notes, PSDText::warpNote));
QVERIFY(notes(source.notes, PSDText::fauxNote));
// The builder reports the parser's notes, in order.
const PSDImport imported = importedFile(QStringLiteral("Arc"), PSDFixture::colorImage(4, 4, 0, 0, 0), 16, block);
QCOMPARE(imported.conversions.at(0).message, PSDText::warpNote);
QCOMPARE(imported.conversions.at(1).message, PSDText::fauxNote);
TypeBlock italic = hello();
italic.fauxItalic = true;
QCOMPARE(parsed(italic).value().notes, std::vector{PSDText::fauxNote});
}
void PSDTextTests::verticalOrBrokenPhotoshopTextStaysPixels()
{
TypeBlock vertical = hello();
vertical.vertical = true;
QVERIFY(!parsed(vertical));
QVERIFY(!PSDText::parse({{QStringLiteral("TySh"), QByteArray("\0\1", 2)}}));
TypeBlock uneven = hello();
uneven.xx = 2;
QVERIFY(!parsed(uneven));
TypeBlock sheared = hello();
sheared.xy = 0.5;
QVERIFY(!parsed(sheared));
const PSDImport imported = importedFile(QStringLiteral("Sideways"), PSDFixture::colorImage(4, 4, 0, 0, 1), 16, vertical);
const ImageLayer &layer = imported.layers.at(0);
QVERIFY(!layer.liveText());
QCOMPARE(layer.asset.value().size(), QSize(4, 4));
QVERIFY(reported(imported, PSDText::rasterizedNote));
}
void PSDTextTests::missingPhotoshopFontIsReportedButStaysEditable()
{
TypeBlock block = hello();
block.font = QStringLiteral("DefinitelyMissingFontXYZ");
PSDRecord record = PSDFixture::record(QStringLiteral("Missing"), PSDFixture::colorImage(2, 2, 0, 0, 0));
record.kind = PSDLayerKind::text;
record.text = parsed(block).value();
const PSDImport imported = PSDDocumentBuilder::makeImport(PSDDocument{64, 64, 72, {record}});
QCOMPARE(imported.layers.at(0).liveText().value().style.fontName, QString("DefinitelyMissingFontXYZ"));
const QString note = PSDText::missingFontNote(QStringLiteral("DefinitelyMissingFontXYZ")).value();
QCOMPARE(note, QString("The font “DefinitelyMissingFontXYZ” isn’t installed, so the text was drawn with a substitute font."));
QVERIFY(reported(imported, note));
// An installed face by its PostScript name says nothing.
QVERIFY(!PSDText::missingFontNote(QStringLiteral("DejaVuSans")));
}
void PSDTextTests::rotatedPhotoshopTextKeepsItsAngle()
{
TypeBlock block = hello();
block.xx = 0;
block.xy = -1;
block.yx = 1;
block.yy = 0;
const PSDText::Source source = parsed(block).value();
QVERIFY(std::abs(source.rotation - 90) < 0.01);
QVERIFY(!source.flipY);
block.xy = 1;
QVERIFY(parsed(block).value().flipY);
// Two per cent of shear or unevenness passes, no more.
const std::vector<std::pair<std::array<double, 4>, bool>> limits{{{1, 0.019, 0, 1}, true}, {{1, 0.021, 0, 1}, false},
{{1, 0, 0, 1.019}, true}, {{1, 0, 0, 1.021}, false},
{{1.019, 0, 0, 1}, true}, {{1.021, 0, 0, 1}, false}};
for (const auto &[matrix, passes] : limits) {
TypeBlock near = hello();
near.xx = matrix[0];
near.xy = matrix[1];
near.yx = matrix[2];
near.yy = matrix[3];
QCOMPARE(parsed(near).has_value(), passes);
}
}
void PSDTextTests::wildNumbersNeverTrap()
{
TypeBlock block = hello();
block.fontIndex = "1e300";
block.justification = "1e300";
const PSDText::Source source = parsed(block).value();
QCOMPARE(source.style.fontName, QString("Helvetica"));
QVERIFY(source.style.alignment == TextAlignment::left);
QCOMPARE(source.notes, std::vector{PSDText::justifyNote});
// Full justification, 3, is left with the note.
block.justification = "3";
QCOMPARE(parsed(block).value().notes, std::vector{PSDText::justifyNote});
block.justification = "1";
QVERIFY(parsed(block).value().style.alignment == TextAlignment::right);
block.fontIndex = "1";
QCOMPARE(parsed(block).value().style.fontName, QString("Helvetica"));
}
void PSDTextTests::theWordsAreCleanedAndMayComeFromTheEngine()
{
TypeBlock block = hello();
block.text = QStringLiteral("") + QChar(0) + QStringLiteral("A\r\nB\rC") + QChar(0);
QCOMPARE(parsed(block).value().style.content, QString("A\nB\nC"));
block.text = QStringLiteral("H") + QChar(0xD800);
QCOMPARE(parsed(block).value().style.content, QStringLiteral("H") + QChar::ReplacementCharacter);
// Without a `Txt ` item the engine's words count.
block.text = QStringLiteral("From the engine");
block.typed = false;
QCOMPARE(parsed(block).value().style.content, QString("From the engine"));
// Empty words stay pixels, never falling back.
block.typed = true;
block.text = QString();
QVERIFY(!parsed(block));
}
void PSDTextTests::framesReadStandardisedAndPlaced()
{
TypeBlock block = hello();
block.tx = 10;
block.ty = 30;
block.bounds = std::array{200.0, 80.0, 0.0, 0.0};
block.glyphBounds = std::array{40.0, 10.0, 0.0, -10.0};
const PSDText::Source flipped = parsed(block).value();
QCOMPARE(flipped.style.boxSize.value(), QSizeF(224, 104));
QCOMPARE(flipped.documentAnchor, QPointF(10, 30));
// Turned a quarter, the frame's corner goes through the matrix.
block.xx = 0;
block.xy = -1;
block.yx = 1;
block.yy = 0;
block.bounds = std::array{5.0, 7.0, 205.0, 87.0};
QCOMPARE(parsed(block).value().documentAnchor, QPointF(3, 35));
// Scaled twice, the frame doubles.
block.xx = block.yy = 2;
block.xy = block.yx = 0;
QCOMPARE(parsed(block).value().style.boxSize.value(), QSizeF(424, 184));
QCOMPARE(parsed(block).value().documentAnchor, QPointF(20, 44));
// Rendered, the frame's padded corner lands on its anchor.
for (const auto &matrix : std::vector<std::array<double, 4>>{{1, 0, 0, 1}, {0, -1, 1, 0}, {0, 1, 1, 0}}) {
block.xx = matrix[0];
block.xy = matrix[1];
block.yx = matrix[2];
block.yy = matrix[3];
const PSDText::Source source = parsed(block).value();
const PSDText::Rendered placed = PSDText::render(source);
QCOMPARE(placed.image.size(), source.style.boxSize.value().toSize());
const double pad = LayerTextStyle::padding;
const QPointF corner = BrushRaster::pixelToDocument(placed.transform, placed.image.width(), placed.image.height()).map(QPointF(pad, pad));
QVERIFY2(std::abs(corner.x() - source.documentAnchor.x()) < 1e-6 && std::abs(corner.y() - source.documentAnchor.y()) < 1e-6,
qPrintable(QString("%1 %2").arg(corner.x()).arg(corner.y())));
}
}
void PSDTextTests::pointTextSitsOnItsBaseline()
{
TypeBlock block = hello();
block.text = QStringLiteral("HHH");
block.font = QStringLiteral("DejaVuSans");
block.fontSize = 40;
const PSDText::Source source = parsed(block).value();
const PSDText::Rendered rendered = PSDText::render(source);
QCOMPARE(rendered.transform.size, QSizeF(rendered.image.size()));
QCOMPARE(rendered.transform.origin.x(), 40 - LayerTextStyle::padding);
// The letters' last inked row ends on the document's baseline.
int bottom = -1;
for (int y = 0; y < rendered.image.height(); ++y)
for (int x = 0; x < rendered.image.width(); ++x)
if (qAlpha(rendered.image.pixel(x, y)) > 128)
bottom = y;
const double baseline = 50 - rendered.transform.origin.y();
QVERIFY(baseline > LayerTextStyle::padding + 20 && baseline < rendered.image.height() - LayerTextStyle::padding);
QVERIFY2(std::abs(bottom + 1 - baseline) < 1, qPrintable(QString("%1 %2").arg(bottom).arg(baseline)));
// Turned and flipped, the anchor still lands on its point.
for (const bool right : {false, true}) {
TypeBlock turned = block;
turned.xx = 0;
turned.xy = 1;
turned.yx = 1;
turned.yy = 0;
turned.justification = right ? "1" : "2";
const PSDText::Source twisted = parsed(turned).value();
QVERIFY(twisted.flipY);
const PSDText::Rendered placed = PSDText::render(twisted);
const double x = right ? placed.image.width() - LayerTextStyle::padding : placed.image.width() / 2.0;
const QPointF lands = BrushRaster::pixelToDocument(placed.transform, placed.image.width(), placed.image.height())
.map(QPointF(x, baseline));
QVERIFY2(std::abs(lands.x() - 40) < 1e-6 && std::abs(lands.y() - 50) < 1e-6, qPrintable(QString("%1 %2").arg(lands.x()).arg(lands.y())));
}
}
void PSDTextTests::textThatCannotBeDrawnStaysPixels()
{
TypeBlock block = hello();
block.tx = 1e300;
const PSDText::Source far = parsed(block).value();
QVERIFY_THROWS_EXCEPTION(ProjectError, PSDText::render(far));
const PSDImport imported = importedFile(QStringLiteral("Far"), PSDFixture::colorImage(4, 4, 1, 0, 0), 16, block);
const ImageLayer &layer = imported.layers.at(0);
QVERIFY(!layer.liveText());
QCOMPARE(layer.asset.value().size(), QSize(4, 4));
QCOMPARE(layer.transform.origin, QPointF(0, 0));
QVERIFY(reported(imported, PSDText::rasterizedNote));
}
void PSDTextTests::theStyleReadsAsSwiftReadsIt()
{
// Runs without a style sheet read nothing, as Swift's fallback.
TypeBlock bare = hello();
bare.sheet = false;
bare.fontSize = 30;
bare.red = 1;
QCOMPARE(parsed(bare).value().style.fontSize, 12.0);
QCOMPARE(parsed(bare).value().style.red, 0.0);
// A size that is no size leaves every default.
TypeBlock zero = hello();
zero.fontSize = 0;
zero.red = 1;
QCOMPARE(parsed(zero).value().style.fontSize, 72.0);
QCOMPARE(parsed(zero).value().style.red, 0.0);
TypeBlock nameless = hello();
nameless.font = QString();
QCOMPARE(parsed(nameless).value().style.fontName, QString("Helvetica"));
TypeBlock infinite = hello();
infinite.tracking = "1e999";
QCOMPARE(parsed(infinite).value().style.tracking, 0.0);
// Automatic leading ignores the stored number.
QCOMPARE(parsed(hello()).value().style.leading, 0.0);
// Channels past one count in bytes.
TypeBlock bytes = hello();
bytes.red = 255;
bytes.green = 51;
const LayerTextStyle coloured = parsed(bytes).value().style;
QCOMPARE(coloured.red, 1.0);
QCOMPARE(coloured.green, 0.2);
// Without an engine the size is twelve, scaled.
TypeBlock plain = hello();
plain.withEngine = false;
plain.xx = plain.yy = 2;
QCOMPARE(parsed(plain).value().style.fontSize, 24.0);
QCOMPARE(parsed(plain).value().style.content, QString("Hello"));
// A frame too short for its letters stays point text.
TypeBlock shallow = hello();
shallow.bounds = std::array{0.0, 0.0, 200.0, 22.0};
shallow.glyphBounds = std::array{0.0, -10.0, 40.0, 10.0};
QVERIFY(!parsed(shallow).value().anchorIsFrame);
// Flipped, the frame's corner flips through the matrix.
TypeBlock flipped = hello();
flipped.tx = 10;
flipped.ty = 30;
flipped.yy = -1;
flipped.bounds = std::array{5.0, 7.0, 205.0, 87.0};
flipped.glyphBounds = std::array{0.0, -10.0, 40.0, 10.0};
const PSDText::Source mirrored = parsed(flipped).value();
QVERIFY(mirrored.flipY);
QCOMPARE(mirrored.documentAnchor, QPointF(15, 23));
// Eight million bytes at most, padding after the warp included.
const QByteArray block = PSDFixture::tySh(hello());
QVERIFY(PSDText::parse({{QStringLiteral("TySh"), block + QByteArray(8'000'000 - block.size(), '\0')}}));
QVERIFY(!PSDText::parse({{QStringLiteral("TySh"), block + QByteArray(8'000'001 - block.size(), '\0')}}));
QVERIFY(PSDText::parse({{QStringLiteral("tySh"), block}}));
}
void PSDTextTests::typeWithAShapeStaysType()
{
// Swift reads the type first: no shape, no mask.
std::vector<PSDFixture::AdditionalLayerInfo> extras{{"TySh", PSDFixture::tySh(hello())}};
PSDVector::Extra shape = PSDVectorFixtures::rectangle();
shape["vogk"] = PSDVectorBuilders::originationData(2, QRectF(945, 153, 646, 182), {0, 0, 0, 0});
QVERIFY(PSDVector::live(shape, PSDVectorFixtures::canvas));
for (const char *key : {"vmsk", "SoCo", "vstk", "vogk"})
extras.push_back({key, shape.at(QString::fromLatin1(key))});
for (const bool pixels : {true, false}) {
const PSDRecord record = pixels ? PSDFixture::record(QStringLiteral("Both"), PSDFixture::colorImage(4, 4, 0, 0, 0), QRectF(0, 0, 4, 4))
: PSDFixture::record(QStringLiteral("Both"));
const PSDDocument document = PSDReader::read(PSDFixture::data(PSDDocument{1920, 1080, 72, {record}}, PSDFixture::colorImage(2, 2, 1, 1, 1), false, extras));
const PSDRecord &read = document.layers.at(0);
QVERIFY(read.kind == PSDLayerKind::text);
QVERIFY(!read.shape);
QCOMPARE(read.image.has_value(), pixels);
QVERIFY(PSDDocumentBuilder::makeImport(document).layers.at(0).liveText());
}
}
QTEST_MAIN(PSDTextTests)
#include "PSDTextTests.moc"
+2
View File
@@ -207,6 +207,8 @@ compositor_test(RemoveBackgroundTests)
compositor_test(SelectSubjectTests)
compositor_test(ObjectSelectionTests)
compositor_test(PSDRoundTripTests)
compositor_test(PSDTextTests)
compositor_test(PSDTextReaderTests)
compositor_test(PSBImportTests)
compositor_test(CropToCanvasImportTests)
compositor_test(PSDVectorTests)