Import simple Photoshop text as editable text (12.11b2)
This commit is contained in:
1 parent
a2241601ec
commit
008493b0a3
20 files changed
+2081
-17
No files matched your search
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
@@ -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 ¬e)
|
||||
{
|
||||
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"
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user