219 lines
7.7 KiB
C++
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
|