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

+31 -7
View File
@@ -8,11 +8,30 @@ ImportedImage imported(const QImage &image, const QString &name)
return ImportedImage(image, PixelAdjust::thumbnail(image), name);
}
std::vector<QString> notes(const PSDRecord &record)
// Swift's `try?`: text that cannot be drawn stays pixels.
std::optional<PSDText::Rendered> rendered(const PSDRecord &record)
{
if (!record.text)
return std::nullopt;
try {
return PSDText::render(*record.text);
} catch (const std::runtime_error &) {
return std::nullopt;
}
}
std::vector<QString> notes(const PSDRecord &record, bool typed)
{
std::vector<QString> result;
if (record.kind == PSDLayerKind::text)
result.push_back(QStringLiteral("Editable Photoshop text becomes pixels and can’t be retyped."));
if (record.kind == PSDLayerKind::text) {
if (typed) {
result.insert(result.end(), record.text->notes.begin(), record.text->notes.end());
if (const std::optional<QString> missing = PSDText::missingFontNote(record.text->style.fontName))
result.push_back(*missing);
} else {
result.push_back(PSDText::rasterizedNote);
}
}
if (record.kind == PSDLayerKind::smartObject)
result.push_back(QStringLiteral("The smart object was rasterized. Linked contents can’t be edited."));
if (record.kind == PSDLayerKind::effects)
@@ -41,10 +60,14 @@ std::vector<QString> notes(const PSDRecord &record)
return result;
}
ImageLayer layerFor(const PSDRecord &record, QSizeF canvas, const std::map<QUuid, ImportedImage> &assets)
ImageLayer layerFor(const PSDRecord &record, QSizeF canvas, const std::map<QUuid, ImportedImage> &assets, const std::optional<PSDText::Rendered> &typed)
{
ImageLayer layer(record.name, canvas);
if (record.image && !record.isGroup && !record.adjustment) {
if (typed && !record.adjustment) {
layer = ImageLayer(imported(typed->image, record.name), QPointF());
layer.transform = typed->transform;
layer.text = LayerText{record.text->style, layer.asset->identity()};
} else if (record.image && !record.isGroup && !record.adjustment) {
const ImportedImage asset = assets.contains(record.id) ? assets.at(record.id) : imported(*record.image, record.name);
const QSizeF size = record.bounds.width() > 0 && record.bounds.height() > 0 ? record.bounds.size() : QSizeF(record.image->size());
layer = ImageLayer(asset, record.bounds.topLeft());
@@ -83,11 +106,12 @@ PSDImport PSDDocumentBuilder::makeImport(const PSDDocument &document, const std:
if (record.croppedToCanvas)
conversions.push_back(PSDConversion{.layerName = record.name,
.message = QStringLiteral("Cropped to the canvas so the file fits in memory. Pixels outside the canvas weren't imported.")});
for (const QString &note : notes(record))
const std::optional<PSDText::Rendered> typed = rendered(record);
for (const QString &note : notes(record, typed.has_value()))
conversions.push_back(PSDConversion{.layerName = record.name, .message = note});
if (record.kind == PSDLayerKind::adjustment && !record.adjustment)
continue;
ImageLayer layer = layerFor(record, canvas, assets);
ImageLayer layer = layerFor(record, canvas, assets, typed);
if (record.mask) {
try {
layer.mask = LayerMask(LayerMask::assetFrom(*record.mask), record.maskEnabled, std::nullopt, record.maskLinked);
+4 -2
View File
@@ -61,7 +61,9 @@ std::vector<PSDRecord> assemble(const std::vector<RawLayer> &raw, QSizeF canvas,
record.opacity = hasEffects && layer.fill != 255 ? layer.opacity / 255.0 : (layer.opacity / 255.0) * (layer.fill / 255.0);
record.bounds = isGroup ? QRectF(QPointF(0, 0), canvas) : QRectF(layer.left, layer.top, std::max<qint64>(0, layer.right - layer.left), std::max<qint64>(0, layer.bottom - layer.top));
record.image = isGroup ? std::nullopt : layer.image;
std::optional<PSDVector::Live> live = isGroup ? std::nullopt : PSDVector::live(layer.extra, canvas, remaining);
if (record.kind == PSDLayerKind::text)
record.text = PSDText::parse(layer.extra);
std::optional<PSDVector::Live> live = isGroup || record.text ? std::nullopt : PSDVector::live(layer.extra, canvas, remaining);
if (live) {
record.image = live->image;
record.bounds = live->bounds;
@@ -69,7 +71,7 @@ std::vector<PSDRecord> assemble(const std::vector<RawLayer> &raw, QSizeF canvas,
record.shapeNotes = live->notes;
record.kind = PSDLayerKind::vector;
remaining = std::max<qint64>(0, remaining - qint64(live->image.width()) * live->image.height());
} else if (!record.image && !isGroup) {
} else if (!record.text && !record.image && !isGroup) {
if (const std::optional<PSDVector::Raster> raster = PSDVector::raster(layer.extra, canvas, remaining)) {
record.image = raster->image;
record.bounds = raster->bounds;
+264
View File
@@ -0,0 +1,264 @@
#include "IO/PSD/PSDText+Descriptor.h"
#include <QStringDecoder>
#include <QtEndian>
#include <bit>
#include <cmath>
namespace PSDDescriptor {
namespace {
// Nesting past this reads as broken, where Swift's stack overflows.
constexpr int maxDepth = 512;
// Swift's ASCII decoding: a high byte fails.
std::optional<QString> ascii(const QByteArray &raw)
{
for (const char byte : raw)
if (uchar(byte) >= 0x80)
return std::nullopt;
return QString::fromLatin1(raw);
}
template <typename T>
const T *find(const Items &items, const QString &key)
{
return items.contains(key) ? std::get_if<T>(&items.at(key).value) : nullptr;
}
// Swift's `??`: the key present decides, then its type.
std::optional<double> side(const Items &items, const QString &name)
{
const double *found = find<double>(items, items.contains(name) ? name : name.trimmed());
return found ? std::optional(*found) : std::nullopt;
}
}
std::optional<QString> string(const Items &items, const QString &key)
{
const QString *found = find<QString>(items, key);
return found ? std::optional(*found) : std::nullopt;
}
std::optional<QString> enumeration(const Items &items, const QString &key)
{
const Enumeration *found = find<Enumeration>(items, key);
return found ? std::optional(found->name) : std::nullopt;
}
std::optional<QByteArray> data(const Items &items, const QString &key)
{
const QByteArray *found = find<QByteArray>(items, key);
return found ? std::optional(*found) : std::nullopt;
}
QString utf16(const QByteArray &raw)
{
QString text = QStringDecoder(QStringConverter::Utf16BE, QStringConverter::Flag::ConvertInitialBom).decode(raw);
for (qsizetype index = 0; index < text.size(); ++index) {
if (text[index].isHighSurrogate() && index + 1 < text.size() && text[index + 1].isLowSurrogate())
++index;
else if (text[index].isSurrogate())
text[index] = QChar::ReplacementCharacter;
}
return text;
}
std::optional<QRectF> rect(const Items &items, const QString &key)
{
const Items *box = find<Items>(items, key);
if (!box)
return std::nullopt;
const std::optional<double> left = side(*box, QStringLiteral("Left")), top = side(*box, QStringLiteral("Top ")),
right = side(*box, QStringLiteral("Rght")), bottom = side(*box, QStringLiteral("Btom"));
if (!left || !top || !right || !bottom)
return std::nullopt;
if (!std::isfinite(*left) || !std::isfinite(*top) || !std::isfinite(*right) || !std::isfinite(*bottom))
return std::nullopt;
return QRectF(QPointF(*left, *top), QPointF(*right, *bottom)).normalized();
}
std::optional<Items> Reader::descriptor(bool versioned)
{
if (versioned && u32() != 16u)
return std::nullopt;
if (!unicode() || !identifier())
return std::nullopt;
const std::optional<quint32> count = u32();
if (!count || *count > 10'000)
return std::nullopt;
Items result;
for (quint32 index = 0; index < *count; ++index) {
const std::optional<QString> key = identifier();
const std::optional<QString> type = key ? fourCC() : std::nullopt;
std::optional<Value> item = type ? value(*type) : std::nullopt;
if (!item)
return std::nullopt;
result.insert_or_assign(*key, std::move(*item));
}
return result;
}
std::optional<Value> Reader::value(const QString &type)
{
if (depth >= maxDepth)
return std::nullopt;
++depth;
std::optional<Value> result = read(type);
--depth;
return result;
}
std::optional<Value> Reader::read(const QString &type)
{
if (type == QLatin1String("doub")) {
const std::optional<double> number = f64();
return number ? std::optional(Value{*number}) : std::nullopt;
}
if (type == QLatin1String("UntF")) {
const std::optional<double> number = fourCC() ? f64() : std::nullopt;
return number ? std::optional(Value{*number}) : std::nullopt;
}
if (type == QLatin1String("long")) {
const std::optional<qint32> number = i32();
return number ? std::optional(Value{double(*number)}) : std::nullopt;
}
if (type == QLatin1String("comp")) {
const std::optional<QByteArray> raw = bytes(8);
return raw ? std::optional(Value{double(qFromBigEndian<qint64>(raw->constData()))}) : std::nullopt;
}
if (type == QLatin1String("bool"))
return u8() ? std::optional(Value{0.0}) : std::nullopt;
if (type == QLatin1String("TEXT")) {
const std::optional<QString> text = unicode();
return text ? std::optional(Value{*text}) : std::nullopt;
}
if (type == QLatin1String("enum")) {
const std::optional<QString> name = identifier() ? identifier() : std::nullopt;
return name ? std::optional(Value{Enumeration{*name}}) : std::nullopt;
}
if (type == QLatin1String("tdta")) {
const std::optional<quint32> length = u32();
const std::optional<QByteArray> raw = length && *length <= 8'000'000 ? bytes(*length) : std::nullopt;
return raw ? std::optional(Value{*raw}) : std::nullopt;
}
if (type == QLatin1String("Objc") || type == QLatin1String("GlbO")) {
std::optional<Items> nested = descriptor(false);
return nested ? std::optional(Value{std::move(*nested)}) : std::nullopt;
}
if (type == QLatin1String("VlLs")) {
const std::optional<quint32> count = u32();
if (!count || *count > 10'000)
return std::nullopt;
std::vector<Value> list;
for (quint32 index = 0; index < *count; ++index) {
const std::optional<QString> itemType = fourCC();
std::optional<Value> item = itemType ? value(*itemType) : std::nullopt;
if (!item)
return std::nullopt;
list.push_back(std::move(*item));
}
return Value{std::move(list)};
}
if (type == QLatin1String("alis")) {
const std::optional<quint32> length = u32();
return length && *length <= 8'000'000 && bytes(*length) ? std::optional(Value{0.0}) : std::nullopt;
}
if (type == QLatin1String("obj "))
return reference() ? std::optional(Value{0.0}) : std::nullopt;
if (type == QLatin1String("type") || type == QLatin1String("GlbC"))
return unicode() && identifier() ? std::optional(Value{0.0}) : std::nullopt;
return std::nullopt;
}
bool Reader::reference()
{
const std::optional<quint32> count = u32();
if (!count || *count > 10'000)
return false;
for (quint32 index = 0; index < *count; ++index) {
const std::optional<QString> form = fourCC();
if (!form)
return false;
bool read = false;
if (*form == QLatin1String("prop"))
read = unicode() && identifier() && identifier();
else if (*form == QLatin1String("Clss"))
read = unicode() && identifier();
else if (*form == QLatin1String("Enmr"))
read = unicode() && identifier() && identifier() && identifier();
else if (*form == QLatin1String("rele"))
read = unicode() && identifier() && i32();
else if (*form == QLatin1String("Idnt") || *form == QLatin1String("indx"))
read = bool(i32());
else if (*form == QLatin1String("name"))
read = bool(unicode());
if (!read)
return false;
}
return true;
}
std::optional<QString> Reader::unicode()
{
const std::optional<quint32> count = u32();
const std::optional<QByteArray> raw = count && *count <= 1'000'000 ? bytes(qint64(*count) * 2) : std::nullopt;
if (!raw)
return std::nullopt;
return utf16(*raw);
}
std::optional<QString> Reader::identifier()
{
const std::optional<quint32> length = u32();
if (!length)
return std::nullopt;
if (*length == 0)
return fourCC();
const std::optional<QByteArray> raw = *length <= 10'000 ? bytes(*length) : std::nullopt;
return raw ? ascii(*raw) : std::nullopt;
}
std::optional<QString> Reader::fourCC()
{
const std::optional<QByteArray> raw = bytes(4);
return raw ? ascii(*raw) : std::nullopt;
}
std::optional<QByteArray> Reader::bytes(qint64 count)
{
if (count < 0 || count > remaining())
return std::nullopt;
const QByteArray slice = data.mid(offset, count);
offset += count;
return slice;
}
std::optional<uchar> Reader::u8()
{
const std::optional<QByteArray> raw = bytes(1);
return raw ? std::optional(uchar(raw->at(0))) : std::nullopt;
}
std::optional<quint16> Reader::u16()
{
const std::optional<QByteArray> raw = bytes(2);
return raw ? std::optional(qFromBigEndian<quint16>(raw->constData())) : std::nullopt;
}
std::optional<quint32> Reader::u32()
{
const std::optional<QByteArray> raw = bytes(4);
return raw ? std::optional(qFromBigEndian<quint32>(raw->constData())) : std::nullopt;
}
std::optional<qint32> Reader::i32()
{
const std::optional<quint32> value = u32();
return value ? std::optional(qint32(*value)) : std::nullopt;
}
std::optional<double> Reader::f64()
{
const std::optional<QByteArray> raw = bytes(8);
return raw ? std::optional(std::bit_cast<double>(qFromBigEndian<quint64>(raw->constData()))) : std::nullopt;
}
}
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <QByteArray>
#include <QRectF>
#include <QString>
#include <map>
#include <optional>
#include <variant>
#include <vector>
// Adobe's action descriptors, as far as a type layer needs.
namespace PSDDescriptor {
struct Value;
using Items = std::map<QString, Value>;
struct Enumeration {
QString name;
};
struct Value {
std::variant<QString, double, Enumeration, QByteArray, Items, std::vector<Value>> value;
};
std::optional<QString> string(const Items &items, const QString &key);
std::optional<QString> enumeration(const Items &items, const QString &key);
std::optional<QByteArray> data(const Items &items, const QString &key);
// Standardised, as CGRect's reads are.
std::optional<QRectF> rect(const Items &items, const QString &key);
// Big-endian UTF-16, a lone surrogate read as U+FFFD.
QString utf16(const QByteArray &raw);
// Big-endian reads over a block; each fails past its end.
struct Reader {
const QByteArray &data;
qsizetype offset = 0;
int depth = 0;
qsizetype remaining() const { return data.size() - offset; }
std::optional<Items> descriptor(bool versioned);
std::optional<QByteArray> bytes(qint64 count);
std::optional<uchar> u8();
std::optional<quint16> u16();
std::optional<quint32> u32();
std::optional<qint32> i32();
std::optional<double> f64();
private:
std::optional<Value> value(const QString &type);
std::optional<Value> read(const QString &type);
bool reference();
std::optional<QString> unicode();
std::optional<QString> identifier();
std::optional<QString> fourCC();
};
}
+323
View File
@@ -0,0 +1,323 @@
#include "IO/PSD/PSDText+Engine.h"
#include "IO/PSD/PSDText+Descriptor.h"
#include <clocale>
#include <cstdlib>
#include <locale.h>
namespace PSDTextEngine {
namespace {
// Nesting past this reads as broken, where Swift's stack overflows.
constexpr int maxDepth = 512;
bool isDelimiter(uchar byte)
{
return byte <= 0x20 || byte == '/' || byte == '<' || byte == '>' || byte == '[' || byte == ']' || byte == '(' || byte == ')';
}
bool isDigit(uchar byte)
{
return byte >= '0' && byte <= '9';
}
std::optional<uchar> hex(uchar byte)
{
if (isDigit(byte))
return uchar(byte - '0');
if (byte >= 'a' && byte <= 'f')
return uchar(byte - 'a' + 10);
if (byte >= 'A' && byte <= 'F')
return uchar(byte - 'A' + 10);
return std::nullopt;
}
// Swift's `Double(text)`: whole text, C numbers, overflow infinite.
std::optional<double> toDouble(const QByteArray &text)
{
static const locale_t c = newlocale(LC_ALL_MASK, "C", locale_t(nullptr));
char *end = nullptr;
const double value = strtod_l(text.constData(), &end, c);
if (end != text.constData() + text.size())
return std::nullopt;
return value;
}
QString decoded(const QByteArray &raw)
{
if (raw.size() >= 2 && uchar(raw[0]) == 0xFE && uchar(raw[1]) == 0xFF) {
// Foundation reads no odd count of UTF-16 bytes.
if (raw.size() % 2 == 1)
return QString();
return PSDDescriptor::utf16(raw.mid(2));
}
return QString::fromLatin1(raw);
}
struct Cursor {
const QByteArray &bytes;
qsizetype index = 0;
int depth = 0;
std::optional<uchar> peek(qsizetype ahead = 0) const
{
const qsizetype at = index + ahead;
return at < bytes.size() ? std::optional(uchar(bytes[at])) : std::nullopt;
}
bool take(const char *token)
{
const QByteArray encoded(token);
if (bytes.mid(index, encoded.size()) != encoded)
return false;
index += encoded.size();
return true;
}
bool takeWord(const char *word)
{
const QByteArray encoded(word);
if (bytes.mid(index, encoded.size()) != encoded)
return false;
const qsizetype after = index + encoded.size();
if (after < bytes.size() && !isDelimiter(uchar(bytes[after])))
return false;
index = after;
return true;
}
void skipWhitespace()
{
while (peek() && (*peek() <= 0x20 || *peek() == '%')) {
if (*peek() == '%') {
while (peek() && *peek() != '\n' && *peek() != '\r')
++index;
} else {
++index;
}
}
}
// Swift's ASCII decoding: a high byte leaves the name empty.
QString readToken()
{
const qsizetype start = index;
while (peek() && !isDelimiter(*peek()))
++index;
const QByteArray token = bytes.mid(start, index - start);
for (const char byte : token)
if (uchar(byte) >= 0x80)
return QString();
return QString::fromLatin1(token);
}
std::optional<Engine> value()
{
if (depth >= maxDepth)
return std::nullopt;
++depth;
std::optional<Engine> result = parseValue();
--depth;
return result;
}
std::optional<Engine> parseValue()
{
skipWhitespace();
const std::optional<uchar> byte = peek();
if (!byte)
return std::nullopt;
if (*byte == '<')
return peek(1) == uchar('<') ? parseDictionary() : parseHex();
if (*byte == '[')
return parseArray();
if (*byte == '(')
return parseString();
if (*byte == '/') {
++index;
return Engine{readToken()};
}
if (*byte == '-' || *byte == '+' || *byte == '.' || isDigit(*byte)) {
const std::optional<double> parsed = parseNumber();
return parsed ? std::optional(Engine{*parsed}) : std::nullopt;
}
if (takeWord("true"))
return Engine{true};
if (takeWord("false"))
return Engine{false};
if (takeWord("null"))
return Engine{QString()};
return std::nullopt;
}
std::optional<Engine> parseDictionary()
{
take("<<");
Dictionary items;
while (true) {
skipWhitespace();
if (!peek() || *peek() == '>')
break;
if (*peek() != '/')
return std::nullopt;
++index;
const QString key = readToken();
std::optional<Engine> item = value();
if (!item)
return std::nullopt;
items.insert_or_assign(key, std::move(*item));
}
if (!take(">>"))
return std::nullopt;
return Engine{std::move(items)};
}
std::optional<Engine> parseArray()
{
take("[");
Array items;
while (true) {
skipWhitespace();
if (!peek() || *peek() == ']')
break;
std::optional<Engine> item = value();
if (!item)
return std::nullopt;
items.push_back(std::move(*item));
}
if (!take("]"))
return std::nullopt;
return Engine{std::move(items)};
}
void digits()
{
while (peek() && isDigit(*peek()))
++index;
}
std::optional<double> parseNumber()
{
const qsizetype start = index;
if (peek() == uchar('+') || peek() == uchar('-'))
++index;
digits();
if (peek() == uchar('.')) {
++index;
digits();
}
if (peek() == uchar('e') || peek() == uchar('E')) {
++index;
if (peek() == uchar('+') || peek() == uchar('-'))
++index;
digits();
}
return toDouble(bytes.mid(start, index - start));
}
std::optional<Engine> parseString()
{
take("(");
QByteArray raw;
while (peek()) {
const uchar byte = *peek();
++index;
if (byte == ')')
break;
if (byte != '\\') {
raw.append(char(byte));
continue;
}
const std::optional<uchar> escaped = peek();
if (!escaped)
return std::nullopt;
++index;
if (*escaped == 'n') {
raw.append('\n');
} else if (*escaped == 'r') {
raw.append('\r');
} else if (*escaped == 't') {
raw.append('\t');
} else if (*escaped >= '0' && *escaped <= '7') {
int octal = *escaped - '0';
for (int count = 0; count < 2 && peek() && *peek() >= '0' && *peek() <= '7'; ++count)
octal = octal * 8 + (bytes[index++] - '0');
raw.append(char(octal & 0xFF));
} else if (*escaped != '\n' && *escaped != '\r') {
raw.append(char(*escaped));
}
}
return Engine{decoded(raw)};
}
std::optional<Engine> parseHex()
{
take("<");
std::vector<uchar> nibbles;
while (peek() && *peek() != '>') {
if (const std::optional<uchar> nibble = hex(*peek()))
nibbles.push_back(*nibble);
++index;
}
if (!take(">"))
return std::nullopt;
QByteArray raw;
for (size_t at = 0; at + 1 < nibbles.size(); at += 2)
raw.append(char(nibbles[at] << 4 | nibbles[at + 1]));
return Engine{decoded(raw)};
}
};
std::optional<Engine> dictionary(const QByteArray &data, qsizetype start)
{
Cursor cursor{data, start};
std::optional<Engine> value = cursor.value();
if (!value || !std::holds_alternative<Dictionary>(value->value))
return std::nullopt;
return value;
}
}
std::optional<Engine> parse(const QByteArray &data)
{
if (std::optional<Engine> found = dictionary(data, 0))
return found;
const qsizetype start = data.indexOf("<<");
if (start <= 0)
return std::nullopt;
return dictionary(data, start);
}
const Engine *walk(const Engine *value, std::initializer_list<const char *> keys)
{
for (const char *key : keys) {
const Dictionary *items = value ? std::get_if<Dictionary>(&value->value) : nullptr;
if (!items || !items->contains(QString::fromLatin1(key)))
return nullptr;
value = &items->at(QString::fromLatin1(key));
}
return value;
}
std::optional<double> number(const Engine *value)
{
const double *found = value ? std::get_if<double>(&value->value) : nullptr;
return found ? std::optional(*found) : std::nullopt;
}
std::optional<bool> boolean(const Engine *value)
{
const bool *found = value ? std::get_if<bool>(&value->value) : nullptr;
return found ? std::optional(*found) : std::nullopt;
}
std::optional<QString> string(const Engine *value)
{
const QString *found = value ? std::get_if<QString>(&value->value) : nullptr;
return found ? std::optional(*found) : std::nullopt;
}
Array array(const Engine *value)
{
const Array *found = value ? std::get_if<Array>(&value->value) : nullptr;
return found ? *found : Array();
}
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <QByteArray>
#include <QString>
#include <map>
#include <optional>
#include <variant>
#include <vector>
// Photoshop's text-engine dictionary, a small PostScript subset.
namespace PSDTextEngine {
struct Engine;
using Dictionary = std::map<QString, Engine>;
using Array = std::vector<Engine>;
struct Engine {
std::variant<double, bool, QString, Dictionary, Array> value;
};
std::optional<Engine> parse(const QByteArray &data);
// Swift's `walk`: nested dictionary keys, else none.
const Engine *walk(const Engine *value, std::initializer_list<const char *> keys);
std::optional<double> number(const Engine *value);
std::optional<bool> boolean(const Engine *value);
std::optional<QString> string(const Engine *value);
// An array's items; anything else reads empty.
Array array(const Engine *value);
}
+287
View File
@@ -0,0 +1,287 @@
#include "IO/PSD/PSDText.h"
#include "Document/EditorSession.h"
#include "IO/PSD/PSDText+Descriptor.h"
#include "IO/PSD/PSDText+Engine.h"
#include "IO/ProjectStore.h"
#include "Rendering/TextLayout.h"
#include <cmath>
#include <numbers>
namespace PSDText {
namespace {
using namespace PSDTextEngine;
// Uniform scale, a turn and a vertical flip; no shear.
struct Placement {
double pixelScale;
double rotation;
bool flipY;
QTransform map;
};
// The matrix alone maps engine sizes to pixels.
std::optional<Placement> placement(double xx, double xy, double yx, double yy, double tx, double ty)
{
const double scaleX = std::hypot(xx, yx);
if (!(scaleX > 1e-6))
return std::nullopt;
const double cosR = xx / scaleX, sinR = yx / scaleX;
const double localX = cosR * xy + sinR * yy;
const double localY = -sinR * xy + cosR * yy;
const double scaleY = std::abs(localY);
if (!(scaleY > 1e-6))
return std::nullopt;
const double largest = std::max(scaleX, scaleY);
if (!(std::abs(localX) <= 0.02 * largest && std::abs(scaleX - scaleY) <= 0.02 * largest))
return std::nullopt;
const double ySign = localY < 0 ? -1.0 : 1.0;
// Swift's exx, eyx, exy and eyy, then the offset.
const QTransform map(cosR * scaleX, sinR * scaleX, -sinR * scaleX * ySign, cosR * scaleX * ySign, tx, ty);
return Placement{scaleX, std::atan2(sinR, cosR) * 180 / std::numbers::pi, localY < 0, map};
}
// Swift's `Int(x.rounded())`, with no trap on a wild value.
std::optional<qint64> rounded(double value)
{
const double whole = std::round(value);
if (!(std::abs(whole) < 9e18))
return std::nullopt;
return qint64(whole);
}
double unit(double value)
{
return value > 1 ? std::min(255.0, std::max(0.0, value)) / 255 : std::min(1.0, std::max(0.0, value));
}
std::array<double, 3> color(const std::vector<double> &values)
{
if (values.size() >= 4)
return {unit(values[1]), unit(values[2]), unit(values[3])};
if (values.size() == 3)
return {unit(values[0]), unit(values[1]), unit(values[2])};
if (!values.empty())
return {unit(values[0]), unit(values[0]), unit(values[0])};
return {0, 0, 0};
}
std::vector<double> channels(const Engine *data)
{
std::vector<double> result;
for (const Engine &value : array(walk(data, {"FillColor", "Values"})))
if (const std::optional<double> channel = number(&value))
result.push_back(*channel);
return result;
}
struct Signature {
double font = 0, size = 0, tracking = 0, leading = 0;
bool autoLeading = true;
double horizontalScale = 1, verticalScale = 1;
bool bold = false, italic = false;
std::array<double, 3> rgb{};
friend bool operator==(const Signature &, const Signature &) = default;
};
Signature signature(const Engine &run)
{
const Engine *sheet = walk(&run, {"StyleSheet", "StyleSheetData"});
const Engine *data = sheet ? sheet : &run;
return Signature{.font = number(walk(data, {"Font"})).value_or(0),
.size = number(walk(data, {"FontSize"})).value_or(0),
.tracking = number(walk(data, {"Tracking"})).value_or(0),
.leading = number(walk(data, {"Leading"})).value_or(0),
.autoLeading = boolean(walk(data, {"AutoLeading"})).value_or(true),
.horizontalScale = number(walk(data, {"HorizontalScale"})).value_or(1),
.verticalScale = number(walk(data, {"VerticalScale"})).value_or(1),
.bold = boolean(walk(data, {"FauxBold"})).value_or(false),
.italic = boolean(walk(data, {"FauxItalic"})).value_or(false),
.rgb = color(channels(data))};
}
void applyStyle(LayerTextStyle &style, const Engine &engine, double pixelScale, std::vector<QString> &notes)
{
const Array runs = array(walk(&engine, {"EngineDict", "StyleRun", "RunArray"}));
const Engine &first = runs.empty() ? engine : runs.front();
// Swift falls back on the run array, where nothing reads.
const Engine *sheet = walk(&first, {"StyleSheet", "StyleSheetData"});
const Engine *data = sheet ? sheet : walk(&engine, {"EngineDict", "StyleRun", "RunArray"});
if (!data)
data = &first;
const double points = number(walk(data, {"FontSize"})).value_or(12);
if (!(std::isfinite(points) && points > 0))
return;
style.fontSize = std::min(2000.0, std::max(1.0, points * pixelScale));
const Array fonts = array(walk(&engine, {"ResourceDict", "FontSet"}));
const std::optional<qint64> index = rounded(number(walk(data, {"Font"})).value_or(0));
if (index && *index >= 0 && *index < qint64(fonts.size())) {
const QString name = string(walk(&fonts[size_t(*index)], {"Name"})).value_or(QString());
if (!name.isEmpty())
style.fontName = name;
}
if (!array(walk(data, {"FillColor", "Values"})).empty()) {
const std::array<double, 3> rgb = color(channels(data));
style.red = rgb[0];
style.green = rgb[1];
style.blue = rgb[2];
}
const double tracking = number(walk(data, {"Tracking"})).value_or(0);
if (std::isfinite(tracking))
style.tracking = std::min(1000.0, std::max(-100.0, tracking * style.fontSize / 1000));
const bool automatic = boolean(walk(data, {"AutoLeading"})).value_or(true);
const std::optional<double> leading = number(walk(data, {"Leading"}));
if (!automatic && leading && std::isfinite(*leading) && *leading > 0)
style.leading = std::min(5000.0, std::max(0.0, *leading * pixelScale));
if (boolean(walk(data, {"FauxBold"})) == true || boolean(walk(data, {"FauxItalic"})) == true)
notes.push_back(fauxNote);
const Signature firstSignature = signature(first);
if (runs.size() > 1 && std::any_of(runs.begin() + 1, runs.end(), [&](const Engine &run) { return signature(run) != firstSignature; }))
notes.push_back(firstStyleNote);
const Array paragraphs = array(walk(&engine, {"EngineDict", "ParagraphRun", "RunArray"}));
const Engine &paragraph = paragraphs.empty() ? engine : paragraphs.front();
const std::optional<qint64> justification = rounded(number(walk(&paragraph, {"ParagraphSheet", "Properties", "Justification"})).value_or(0));
if (justification == 1) {
style.alignment = TextAlignment::right;
} else if (justification == 2) {
style.alignment = TextAlignment::center;
} else {
style.alignment = TextAlignment::left;
if (justification != 0)
notes.push_back(justifyNote);
}
}
std::optional<QString> cleaned(std::optional<QString> text)
{
if (!text)
return std::nullopt;
while (!text->isEmpty() && (text->front() == QChar(0xFEFF) || text->front() == QChar(0)))
text->remove(0, 1);
while (!text->isEmpty() && text->back() == QChar(0))
text->chop(1);
text->replace(QStringLiteral("\r\n"), QStringLiteral("\n"));
text->replace(QChar('\r'), QChar('\n'));
return text;
}
double horizontalAnchor(const LayerTextStyle &style, double width)
{
switch (style.alignment) {
case TextAlignment::left:
return LayerTextStyle::padding;
case TextAlignment::center:
return width / 2;
case TextAlignment::right:
return width - LayerTextStyle::padding;
}
Q_UNREACHABLE();
}
// The first line's baseline, laid as `textImage` lays it.
double baseline(const LayerTextStyle &style, QSizeF image)
{
const double padding = LayerTextStyle::padding;
const TextLines lines(style, QSizeF(std::max(1.0, image.width() - 2 * padding), std::max(1.0, image.height() - 2 * padding)));
return padding + lines.baseline(0);
}
// As `pixelToDocument`: flip, then a clockwise turn about the centre.
LayerTransform layerTransform(QSizeF image, QPointF imageAnchor, QPointF documentAnchor, double rotation, bool flipY)
{
QPointF local(imageAnchor.x() - image.width() / 2, imageAnchor.y() - image.height() / 2);
if (flipY)
local.setY(-local.y());
const double radians = rotation * std::numbers::pi / 180;
const QPointF turned(local.x() * std::cos(radians) - local.y() * std::sin(radians), local.x() * std::sin(radians) + local.y() * std::cos(radians));
const QPointF centre = documentAnchor - turned;
LayerTransform transform{QPointF(centre.x() - image.width() / 2, centre.y() - image.height() / 2), image};
transform.rotation = rotation;
transform.flipY = flipY;
return transform;
}
}
std::optional<QString> missingFontNote(const QString &name)
{
if (TextLayout::availableFonts().contains(name))
return std::nullopt;
return QStringLiteral("The font “%1” isn’t installed, so the text was drawn with a substitute font.").arg(name);
}
std::optional<Source> parse(const std::map<QString, QByteArray> &extra)
{
const QString key = extra.contains(QStringLiteral("TySh")) ? QStringLiteral("TySh") : QStringLiteral("tySh");
if (!extra.contains(key) || extra.at(key).size() > 8'000'000)
return std::nullopt;
PSDDescriptor::Reader reader{extra.at(key)};
if (reader.u16() != 1u)
return std::nullopt;
std::array<double, 6> matrix{};
for (double &value : matrix) {
const std::optional<double> read = reader.f64();
if (!read || !std::isfinite(*read))
return std::nullopt;
value = *read;
}
if (reader.u16() != 50u)
return std::nullopt;
const std::optional<PSDDescriptor::Items> text = reader.descriptor(true);
if (!text || PSDDescriptor::enumeration(*text, QStringLiteral("Ornt")) == QStringLiteral("Vrtc"))
return std::nullopt;
const std::optional<Placement> placed = placement(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
if (!placed)
return std::nullopt;
std::vector<QString> notes;
if (reader.remaining() >= 2 && reader.u16() == 1u) {
const std::optional<PSDDescriptor::Items> warp = reader.descriptor(true);
const std::optional<QString> style = warp ? PSDDescriptor::enumeration(*warp, QStringLiteral("warpStyle")) : std::nullopt;
if (style && *style != QLatin1String("warpNone") && *style != QLatin1String("none"))
notes.push_back(warpNote);
}
const std::optional<QByteArray> engineData = PSDDescriptor::data(*text, QStringLiteral("EngineData"));
const std::optional<Engine> engine = engineData ? PSDTextEngine::parse(*engineData) : std::nullopt;
std::optional<QString> typed = PSDDescriptor::string(*text, QStringLiteral("Txt "));
if (!typed)
typed = PSDDescriptor::string(*text, QStringLiteral("Txt"));
std::optional<QString> content = cleaned(typed);
if (!content && engine)
content = cleaned(string(walk(&*engine, {"EngineDict", "Editor", "Text"})));
if (!content || content->isEmpty() || content->size() > 100'000)
return std::nullopt;
LayerTextStyle style;
style.content = *content;
if (engine)
applyStyle(style, *engine, placed->pixelScale, notes);
else
style.fontSize = std::min(2000.0, std::max(1.0, 12 * placed->pixelScale));
QPointF anchor(matrix[4], matrix[5]);
bool anchorIsFrame = false;
const std::optional<QRectF> bounds = PSDDescriptor::rect(*text, QStringLiteral("bounds"));
const std::optional<QRectF> glyphs = PSDDescriptor::rect(*text, QStringLiteral("boundingBox"));
if (bounds && glyphs && bounds->width() > glyphs->width() + 4 && bounds->height() > glyphs->height() + 4) {
const double pad = LayerTextStyle::padding;
style.boxSize = QSizeF(bounds->width() * placed->pixelScale + pad * 2, bounds->height() * placed->pixelScale + pad * 2);
anchor = placed->map.map(bounds->topLeft());
anchorIsFrame = true;
}
if (!style.isValid())
return std::nullopt;
return Source{style, notes, anchor, placed->rotation, placed->flipY, anchorIsFrame};
}
Rendered render(const Source &source)
{
const QImage image = EditorSession::textImage(source.style);
const QSizeF size = image.size();
const QPointF anchor = source.anchorIsFrame ? QPointF(LayerTextStyle::padding, LayerTextStyle::padding)
: QPointF(horizontalAnchor(source.style, size.width()), baseline(source.style, size));
const LayerTransform transform = layerTransform(size, anchor, source.documentAnchor, source.rotation, source.flipY);
if (!transform.isValid())
throw ProjectError(ProjectError::Kind::tooLarge);
return Rendered{image, transform};
}
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include "Document/TypeTool.h"
#include <QByteArray>
#include <QImage>
#include <map>
#include <optional>
#include <vector>
// Photoshop 6 type (`TySh`) read into the text model.
namespace PSDText {
struct Source {
LayerTextStyle style;
std::vector<QString> notes;
// The document point the image's anchor lands on.
QPointF documentAnchor;
double rotation = 0;
bool flipY = false;
// The paragraph frame's corner, else point text's baseline.
bool anchorIsFrame = false;
};
struct Rendered {
QImage image;
LayerTransform transform;
};
inline const QString rasterizedNote = QStringLiteral("Editable Photoshop text becomes pixels and can’t be retyped.");
inline const QString firstStyleNote = QStringLiteral("Only the first text style was kept.");
inline const QString warpNote = QStringLiteral("The Photoshop text warp was omitted.");
inline const QString fauxNote = QStringLiteral("Faux bold or faux italic was omitted.");
inline const QString justifyNote = QStringLiteral("Full justification was imported as left alignment.");
std::optional<QString> missingFontNote(const QString &name);
std::optional<Source> parse(const std::map<QString, QByteArray> &extra);
Rendered render(const Source &source);
}
+3
View File
@@ -1,5 +1,6 @@
#pragma once
#include "Document/EditorSession+Model.h"
#include "IO/PSD/PSDText.h"
#include <QImage>
#include <QRectF>
#include <QString>
@@ -43,6 +44,8 @@ struct PSDRecord {
PSDLayerKind kind = PSDLayerKind::raster;
std::optional<LayerShapeStyle> shape;
std::vector<QString> shapeNotes;
// Photoshop type the text model can hold.
std::optional<PSDText::Source> text;
// Cut to the canvas so the file fits the budget.
bool croppedToCanvas = false;
std::optional<LayerBlendMode> blendMode() const;
+7
View File
@@ -220,6 +220,13 @@ std::optional<QRectF> TextLines::caret(int position) const
return QRectF(x, *line * m_lineHeight, 1, m_lineHeight);
}
double TextLines::baseline(int line) const
{
const Line &laid = m_lineList[size_t(line)];
const QTextLine text = m_paragraphs[laid.paragraph]->lineAt(laid.index);
return text.position().y() + text.ascent();
}
int TextLines::lineStart(int line) const
{
const Line &laid = m_lineList[size_t(line)];
+2
View File
@@ -43,6 +43,8 @@ public:
std::optional<int> lineOf(int position) const;
// The caret before a position: a unit wide.
std::optional<QRectF> caret(int position) const;
// A laid line's baseline from the container's top.
double baseline(int line) const;
// A line's first position; its last before any wrap.
int lineStart(int line) const;
int lineEnd(int line) const;