Files
SpineParticlesWeb/DateToSpine/src/conversion/ConversionJob.cpp
T
2026-09-07 21:29:15 +08:00

219 lines
7.7 KiB
C++

#include "conversion/ConversionJob.hpp"
#include "atlas/AtlasParser.hpp"
#include "domain/SpineVersion.hpp"
#include "editor/SpineVersionAvailability.hpp"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QRegularExpression>
#include <QTemporaryDir>
#include <cmath>
#include <filesystem>
namespace dts {
namespace {
std::filesystem::path localPath(const QString& value) {
#if defined(_WIN32)
return std::filesystem::path(value.toStdWString());
#else
return std::filesystem::path(value.toStdString());
#endif
}
QString uniqueDirectory(const QString& parent, const QString& name) {
QDir directory(parent);
QString candidate = directory.filePath(name);
for (int suffix = 2; QFileInfo::exists(candidate); ++suffix)
candidate = directory.filePath(QStringLiteral("%1-%2").arg(name).arg(suffix));
return candidate;
}
} // namespace
ConversionJob::ConversionJob(ConversionRequest request, QObject* parent)
: QObject(parent), request_(std::move(request)) {}
ConversionJob::~ConversionJob() = default;
void ConversionJob::start() {
QString error;
if (!prepare(error)) {
fail(error);
return;
}
startUnpack();
}
bool ConversionJob::prepare(QString& error) {
const auto version = SpineVersion::parse(request_.editorVersion.toStdString());
if (!version || !SpineVersionAvailability{}.isCached(*version)) {
error = tr("本地未缓存 Spine %1。为避免触发下载,转换已停止。")
.arg(request_.editorVersion);
return false;
}
if (!QFileInfo::exists(request_.editorPath)) {
error = tr("找不到已指定的 Spine Editor。请重新指定程序路径。");
return false;
}
const auto atlas = AtlasParser{}.parseFile(localPath(request_.atlasPath));
if (!atlas.document || atlas.document->pages.empty()) {
error = tr("Atlas 无法识别,不能确定官方导入比例。");
return false;
}
double scale = atlas.document->pages.front().scale.value_or(1.0);
if (!std::isfinite(scale) || scale <= 0.0) {
error = tr("Atlas 中的 scale 无效。");
return false;
}
for (const auto& page : atlas.document->pages) {
const double pageScale = page.scale.value_or(1.0);
if (!std::isfinite(pageScale) || std::abs(pageScale - scale) > 0.000001) {
error = tr("Atlas 的纹理页使用了不同 scale,无法生成尺寸一致的工程。");
return false;
}
}
importScale_ = QString::number(scale, 'g', 15);
QDir parent(request_.outputParent);
if (!parent.exists() && !parent.mkpath(QStringLiteral("."))) {
error = tr("无法创建输出目录。");
return false;
}
finalDirectory_ = uniqueDirectory(request_.outputParent, request_.assetName);
staging_ = std::make_unique<QTemporaryDir>(
parent.filePath(QStringLiteral(".datetospine-XXXXXX")));
if (!staging_->isValid()) {
error = tr("无法创建临时转换目录。");
return false;
}
QDir staging(staging_->path());
if (!staging.mkpath(QStringLiteral("images"))) {
error = tr("无法准备图片目录。");
return false;
}
projectPath_ = staging.filePath(request_.assetName + QStringLiteral(".spine"));
// The official unpacker reads `pma` from the Atlas. When conversion to
// straight alpha is requested, pass the source Atlas unchanged so Spine
// performs the conversion itself. Otherwise a temporary metadata copy
// disables that conversion while leaving all geometry fields intact.
unpackAtlasPath_ = request_.unpremultiplyAlpha
? request_.atlasPath
: staging.filePath(QStringLiteral("unpack.atlas"));
return writeUnpackAtlas(error);
}
bool ConversionJob::writeUnpackAtlas(QString& error) {
if (request_.unpremultiplyAlpha) return true;
QFile source(request_.atlasPath);
if (!source.open(QIODevice::ReadOnly)) {
error = tr("无法读取 Atlas 文件。");
return false;
}
auto contents = QString::fromUtf8(source.readAll());
source.close();
// Preserve packed pixel values when the user explicitly leaves PMA enabled.
// Pixel decoding, cropping and Alpha handling remain entirely inside Spine.
contents.replace(QRegularExpression(
QStringLiteral("(?m)^(\\s*pma\\s*:\\s*)true(\\s*)$")), QStringLiteral("\\1false\\2"));
QFile temporary(unpackAtlasPath_);
const auto encoded = contents.toUtf8();
if (!temporary.open(QIODevice::WriteOnly | QIODevice::Truncate)
|| temporary.write(encoded) != encoded.size()) {
error = tr("无法准备离线图集解包配置。");
return false;
}
return true;
}
void ConversionJob::startUnpack() {
stage_ = Stage::Unpacking;
process_ = new QProcess(this);
process_->setProcessChannelMode(QProcess::MergedChannels);
connect(process_, &QProcess::finished, this, &ConversionJob::processFinished);
connect(process_, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
if (error == QProcess::FailedToStart && stage_ != Stage::Done)
fail(tr("无法启动本机 Spine Editor。"));
});
process_->start(request_.editorPath, {
"-u", request_.editorVersion,
"-i", QFileInfo(request_.atlasPath).absolutePath(),
"-o", QDir(staging_->path()).filePath(QStringLiteral("images")),
"-c", unpackAtlasPath_});
}
void ConversionJob::startImport() {
stage_ = Stage::Importing;
process_->start(request_.editorPath, {
"-u", request_.editorVersion,
"-i", request_.skeletonPath,
// Numeric scale works with all supported Editor lines. Older Editors
// (notably 4.0) do not accept an Atlas path for this argument.
"-s", importScale_,
"-o", projectPath_,
"--import"});
}
bool ConversionJob::hasUnpackedImages() const {
QDirIterator iterator(QDir(staging_->path()).filePath(QStringLiteral("images")),
{"*.png", "*.jpg", "*.jpeg", "*.webp"}, QDir::Files,
QDirIterator::Subdirectories);
return iterator.hasNext();
}
void ConversionJob::processFinished(int exitCode) {
if (stage_ == Stage::Done) return;
const auto output = QString::fromUtf8(process_->readAll());
if (exitCode != 0) {
fail(stage_ == Stage::Unpacking
? tr("官方图集解包失败:\n%1").arg(output.right(1600))
: tr("Spine 工程导入失败:\n%1").arg(output.right(1600)));
return;
}
if (stage_ == Stage::Unpacking) {
if (!hasUnpackedImages()) {
fail(tr("官方图集解包未生成任何图片。"));
return;
}
if (unpackAtlasPath_ != request_.atlasPath) QFile::remove(unpackAtlasPath_);
startImport();
return;
}
if (stage_ == Stage::Importing) {
if (QFileInfo(projectPath_).size() <= 0) {
fail(tr("Spine Editor 未生成有效的 .spine 工程。"));
return;
}
publish();
}
}
void ConversionJob::publish() {
stage_ = Stage::Publishing;
const auto source = localPath(staging_->path());
const auto destination = localPath(finalDirectory_);
std::error_code error;
std::filesystem::rename(source, destination, error);
if (error) {
fail(tr("无法发布转换结果:%1").arg(QString::fromStdString(error.message())));
return;
}
staging_->setAutoRemove(false);
stage_ = Stage::Done;
emit finished(true, finalDirectory_, {});
}
void ConversionJob::fail(const QString& reason) {
if (stage_ == Stage::Done) return;
stage_ = Stage::Done;
if (process_) process_->kill();
emit finished(false, {}, reason);
}
} // namespace dts