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
@@ -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 ¬e : notes(record))
|
||||
const std::optional<PSDText::Rendered> typed = rendered(record);
|
||||
for (const QString ¬e : 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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> ¬es)
|
||||
{
|
||||
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 ¶graph = paragraphs.empty() ? engine : paragraphs.front();
|
||||
const std::optional<qint64> justification = rounded(number(walk(¶graph, {"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};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user