#include "conversion/ConversionJob.hpp" #include "atlas/AtlasParser.hpp" #include "domain/SpineVersion.hpp" #include "editor/SpineVersionAvailability.hpp" #include #include #include #include #include #include #include #include #include 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( 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