362 lines
12 KiB
C++
362 lines
12 KiB
C++
#pragma once
|
|
#include "Document/BrushStroke.h"
|
|
#include "IO/PSD/PSDTypes.h"
|
|
#include <QPainter>
|
|
|
|
// Swift's PSDFixture: tiny Photoshop files; the app writes none.
|
|
namespace PSDFixture {
|
|
struct Buffer {
|
|
QByteArray data;
|
|
void u8(uchar value) { data.append(char(value)); }
|
|
void u16(quint16 value)
|
|
{
|
|
u8(uchar(value >> 8));
|
|
u8(uchar(value));
|
|
}
|
|
void i16(qint16 value) { u16(quint16(value)); }
|
|
void u32(quint32 value)
|
|
{
|
|
u16(quint16(value >> 16));
|
|
u16(quint16(value));
|
|
}
|
|
void i32(qint32 value) { u32(quint32(value)); }
|
|
void bytes(const QByteArray &value) { data.append(value); }
|
|
void string(const char *value) { data.append(value); }
|
|
};
|
|
|
|
struct Planes {
|
|
std::vector<uchar> red, green, blue, alpha;
|
|
};
|
|
|
|
struct Channel {
|
|
qint16 id;
|
|
QByteArray payload;
|
|
};
|
|
|
|
struct Prepared {
|
|
PSDRecord record;
|
|
bool isDivider = false;
|
|
std::vector<Channel> channels;
|
|
int top = 0, left = 0, bottom = 0, right = 0;
|
|
int maskTop = 0, maskLeft = 0, maskBottom = 0, maskRight = 0;
|
|
};
|
|
|
|
// Straight colour from premultiplied pixels, rounded as Swift rounds.
|
|
inline Planes planes(const QImage &image)
|
|
{
|
|
const QImage pixels = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
|
Planes result;
|
|
for (int y = 0; y < pixels.height(); ++y) {
|
|
const uchar *row = pixels.constScanLine(y);
|
|
for (int x = 0; x < pixels.width(); ++x) {
|
|
const int r = row[x * 4], g = row[x * 4 + 1], b = row[x * 4 + 2], a = row[x * 4 + 3];
|
|
result.alpha.push_back(uchar(a));
|
|
const auto straight = [a](int value) { return a == 0 ? uchar(0) : uchar(std::min(255, (value * 255 + a / 2) / a)); };
|
|
result.red.push_back(straight(r));
|
|
result.green.push_back(straight(g));
|
|
result.blue.push_back(straight(b));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
inline std::vector<uchar> grayPlane(const QImage &image)
|
|
{
|
|
const QImage gray = image.convertToFormat(QImage::Format_Grayscale8);
|
|
std::vector<uchar> plane;
|
|
for (int y = 0; y < gray.height(); ++y)
|
|
plane.insert(plane.end(), gray.constScanLine(y), gray.constScanLine(y) + gray.width());
|
|
return plane;
|
|
}
|
|
|
|
inline QByteArray packBits(const std::vector<uchar> &row)
|
|
{
|
|
QByteArray output;
|
|
size_t i = 0;
|
|
while (i < row.size()) {
|
|
if (i + 1 < row.size() && row[i] == row[i + 1]) {
|
|
size_t run = 2;
|
|
while (i + run < row.size() && row[i + run] == row[i] && run < 128)
|
|
++run;
|
|
output.append(char(qint8(1 - int(run))));
|
|
output.append(char(row[i]));
|
|
i += run;
|
|
} else {
|
|
const size_t start = i++;
|
|
while (i < row.size() && i - start < 128) {
|
|
if (i + 1 < row.size() && row[i] == row[i + 1])
|
|
break;
|
|
++i;
|
|
}
|
|
output.append(char(i - start - 1));
|
|
output.append(reinterpret_cast<const char *>(row.data() + start), qsizetype(i - start));
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
// PackBits by row: the counts, then the rows.
|
|
inline QByteArray encode(const std::vector<uchar> &plane, int width, int height)
|
|
{
|
|
Buffer counts;
|
|
QByteArray packed;
|
|
for (int row = 0; row < height; ++row) {
|
|
const QByteArray line = packBits(std::vector<uchar>(plane.begin() + row * width, plane.begin() + (row + 1) * width));
|
|
counts.u16(quint16(line.size()));
|
|
packed.append(line);
|
|
}
|
|
return counts.data + packed;
|
|
}
|
|
|
|
inline QByteArray channelPayload(const std::vector<uchar> &plane, int width, int height)
|
|
{
|
|
return QByteArray("\0\1", 2) + encode(plane, width, height);
|
|
}
|
|
|
|
inline std::vector<Channel> emptyChannels()
|
|
{
|
|
const QByteArray raw("\0\0", 2);
|
|
return {{-1, raw}, {0, raw}, {1, raw}, {2, raw}};
|
|
}
|
|
|
|
inline Prepared layer(const PSDRecord &record)
|
|
{
|
|
const int width = record.image ? record.image->width() : 0, height = record.image ? record.image->height() : 0;
|
|
const int left = int(std::round(record.bounds.left())), top = int(std::round(record.bounds.top()));
|
|
Prepared prepared;
|
|
prepared.record = record;
|
|
if (record.image && width > 0 && height > 0) {
|
|
const Planes split = planes(*record.image);
|
|
for (const auto &[id, plane] : {std::pair<qint16, const std::vector<uchar> &>(-1, split.alpha), {0, split.red}, {1, split.green}, {2, split.blue}})
|
|
prepared.channels.push_back({id, channelPayload(plane, width, height)});
|
|
} else {
|
|
prepared.channels = emptyChannels();
|
|
}
|
|
if (record.mask)
|
|
prepared.channels.push_back({-2, channelPayload(grayPlane(*record.mask), record.mask->width(), record.mask->height())});
|
|
prepared.top = top;
|
|
prepared.left = left;
|
|
prepared.bottom = top + height;
|
|
prepared.right = left + width;
|
|
prepared.maskTop = top;
|
|
prepared.maskLeft = left;
|
|
prepared.maskBottom = top + (record.mask ? record.mask->height() : 0);
|
|
prepared.maskRight = left + (record.mask ? record.mask->width() : 0);
|
|
return prepared;
|
|
}
|
|
|
|
inline Prepared emptyLayer(const QString &name, const QString &blendKey, int section, std::optional<QUuid> parent, const PSDRecord *group = nullptr)
|
|
{
|
|
PSDRecord record;
|
|
record.id = group ? group->id : QUuid::createUuid();
|
|
record.parentID = parent;
|
|
record.name = name;
|
|
record.isGroup = section != 3;
|
|
record.isVisible = group ? group->isVisible : true;
|
|
record.opacity = group ? group->opacity : 1;
|
|
record.blendKey = blendKey;
|
|
record.mask = group ? group->mask : std::nullopt;
|
|
record.maskEnabled = group ? group->maskEnabled : true;
|
|
record.kind = PSDLayerKind::group;
|
|
Prepared prepared;
|
|
prepared.record = record;
|
|
prepared.isDivider = section == 3;
|
|
prepared.channels = emptyChannels();
|
|
if (record.mask) {
|
|
prepared.channels.push_back({-2, channelPayload(grayPlane(*record.mask), record.mask->width(), record.mask->height())});
|
|
prepared.maskBottom = record.mask->height();
|
|
prepared.maskRight = record.mask->width();
|
|
}
|
|
return prepared;
|
|
}
|
|
|
|
inline void additional(Buffer &buffer, const char *key, const QByteArray &payload)
|
|
{
|
|
buffer.string("8BIM");
|
|
buffer.string(key);
|
|
buffer.u32(quint32(payload.size()));
|
|
buffer.bytes(payload);
|
|
if (payload.size() % 2 == 1)
|
|
buffer.u8(0);
|
|
}
|
|
|
|
inline QByteArray luni(const QString &name)
|
|
{
|
|
Buffer data;
|
|
data.u32(quint32(name.size()));
|
|
for (const QChar unit : name)
|
|
data.u16(unit.unicode());
|
|
return data.data;
|
|
}
|
|
|
|
inline QByteArray extraData(const Prepared &item)
|
|
{
|
|
Buffer extra;
|
|
if (item.record.mask && item.maskRight > item.maskLeft && item.maskBottom > item.maskTop) {
|
|
extra.u32(20);
|
|
extra.i32(item.maskTop);
|
|
extra.i32(item.maskLeft);
|
|
extra.i32(item.maskBottom);
|
|
extra.i32(item.maskRight);
|
|
extra.u8(255);
|
|
extra.u8(uchar((item.record.maskLinked ? 0 : 1) | (item.record.maskEnabled ? 0 : 2)));
|
|
extra.u16(0);
|
|
} else {
|
|
extra.u32(0);
|
|
}
|
|
extra.u32(0);
|
|
const QByteArray pascal = item.record.name.toUtf8().left(255);
|
|
extra.u8(uchar(pascal.size()));
|
|
extra.bytes(pascal);
|
|
extra.bytes(QByteArray((4 - ((1 + pascal.size()) % 4)) % 4, '\0'));
|
|
additional(extra, "luni", luni(item.record.name));
|
|
if (item.record.isGroup || item.isDivider) {
|
|
Buffer payload;
|
|
payload.u32(item.isDivider ? 3 : 1);
|
|
payload.string("8BIM");
|
|
payload.bytes((item.isDivider ? QStringLiteral("norm") : item.record.blendKey + QStringLiteral(" ")).left(4).toLatin1());
|
|
additional(extra, "lsct", payload.data);
|
|
}
|
|
return extra.data;
|
|
}
|
|
|
|
inline void writeRecord(Buffer &buffer, const Prepared &item)
|
|
{
|
|
buffer.i32(item.top);
|
|
buffer.i32(item.left);
|
|
buffer.i32(item.bottom);
|
|
buffer.i32(item.right);
|
|
buffer.u16(quint16(item.channels.size()));
|
|
for (const Channel &channel : item.channels) {
|
|
buffer.i16(channel.id);
|
|
buffer.u32(quint32(channel.payload.size()));
|
|
}
|
|
buffer.string("8BIM");
|
|
buffer.bytes((item.record.blendKey + QStringLiteral(" ")).left(4).toLatin1());
|
|
buffer.u8(uchar(std::clamp(int(std::round(item.record.opacity * 255)), 0, 255)));
|
|
buffer.u8(item.record.clipping ? 1 : 0);
|
|
buffer.u8(item.record.isVisible ? 0 : 2);
|
|
buffer.u8(0);
|
|
const QByteArray extra = extraData(item);
|
|
buffer.u32(quint32(extra.size()));
|
|
buffer.bytes(extra);
|
|
}
|
|
|
|
inline void emitLayers(const PSDDocument &document, std::optional<QUuid> parent, std::vector<Prepared> &prepared)
|
|
{
|
|
// File order runs bottom to top: divider, children, folder.
|
|
for (const PSDRecord &record : document.layers) {
|
|
if (record.parentID != parent)
|
|
continue;
|
|
if (record.isGroup) {
|
|
prepared.push_back(emptyLayer(QStringLiteral("</Layer group>"), QStringLiteral("norm"), 3, parent));
|
|
emitLayers(document, record.id, prepared);
|
|
prepared.push_back(emptyLayer(record.name, record.blendKey, 1, record.parentID, &record));
|
|
} else {
|
|
prepared.push_back(layer(record));
|
|
}
|
|
}
|
|
}
|
|
|
|
inline QByteArray layerSection(const PSDDocument &document)
|
|
{
|
|
std::vector<Prepared> prepared;
|
|
emitLayers(document, std::nullopt, prepared);
|
|
Buffer records, payloads;
|
|
records.i16(qint16(prepared.size()));
|
|
for (const Prepared &item : prepared) {
|
|
writeRecord(records, item);
|
|
for (const Channel &channel : item.channels)
|
|
payloads.bytes(channel.payload);
|
|
}
|
|
Buffer info;
|
|
info.u32(0);
|
|
info.bytes(records.data);
|
|
info.bytes(payloads.data);
|
|
if (info.data.size() % 2 == 1)
|
|
info.u8(0);
|
|
Buffer length;
|
|
length.u32(quint32(info.data.size() - 4));
|
|
info.data.replace(0, 4, length.data);
|
|
info.u32(0);
|
|
return info.data;
|
|
}
|
|
|
|
inline QByteArray resolutionResource(double resolution)
|
|
{
|
|
Buffer resource;
|
|
resource.string("8BIM");
|
|
resource.u16(1005);
|
|
resource.u8(0);
|
|
resource.u8(0);
|
|
resource.u32(16);
|
|
const quint32 fixed = quint32(std::round(std::clamp(resolution, 1.0, 9600.0) * 65536));
|
|
for (int axis = 0; axis < 2; ++axis) {
|
|
resource.u32(fixed);
|
|
resource.u16(1);
|
|
resource.u16(1);
|
|
}
|
|
return resource.data;
|
|
}
|
|
|
|
inline QByteArray data(const PSDDocument &document, const QImage &composite)
|
|
{
|
|
Buffer file;
|
|
file.string("8BPS");
|
|
file.u16(1);
|
|
file.bytes(QByteArray(6, '\0'));
|
|
file.u16(4);
|
|
file.u32(quint32(document.height));
|
|
file.u32(quint32(document.width));
|
|
file.u16(8);
|
|
file.u16(3);
|
|
file.u32(0);
|
|
const QByteArray resources = resolutionResource(document.resolution);
|
|
file.u32(quint32(resources.size()));
|
|
file.bytes(resources);
|
|
const QByteArray layers = layerSection(document);
|
|
file.u32(quint32(layers.size()));
|
|
file.bytes(layers);
|
|
// The composite, PackBits by channel.
|
|
QImage flattened = BrushRaster::context(document.width, document.height, false);
|
|
QPainter painter(&flattened);
|
|
BrushRaster::draw(composite, QRectF(0, 0, document.width, document.height), painter);
|
|
painter.end();
|
|
const Planes split = planes(flattened);
|
|
file.u16(1);
|
|
QByteArray counts, packed;
|
|
for (const std::vector<uchar> *plane : {&split.red, &split.green, &split.blue, &split.alpha}) {
|
|
const QByteArray encoded = encode(*plane, document.width, document.height);
|
|
counts.append(encoded.left(document.height * 2));
|
|
packed.append(encoded.mid(document.height * 2));
|
|
}
|
|
file.bytes(counts);
|
|
file.bytes(packed);
|
|
return file.data;
|
|
}
|
|
|
|
inline QImage colorImage(int width, int height, double red, double green, double blue, double alpha = 1)
|
|
{
|
|
QImage image = BrushRaster::context(width, height, false);
|
|
image.fill(QColor::fromRgbF(float(red), float(green), float(blue), float(alpha)));
|
|
return image;
|
|
}
|
|
|
|
inline QImage grayImage(int width, int height, int value)
|
|
{
|
|
QImage image = BrushRaster::context(width, height, true);
|
|
image.fill(value);
|
|
return image;
|
|
}
|
|
|
|
inline PSDRecord record(const QString &name, std::optional<QImage> image = std::nullopt, QRectF bounds = QRectF())
|
|
{
|
|
PSDRecord result;
|
|
result.id = QUuid::createUuid();
|
|
result.name = name;
|
|
result.image = image;
|
|
result.bounds = bounds;
|
|
return result;
|
|
}
|
|
}
|