Implement a unified export system for extracting data from parsed files: ExportManager (singleton): - Centralized export handling for all content types - Content type detection (image, audio, video, text, binary) - Batch export support with progress tracking Format-specific export dialogs: - ImageExportDialog: PNG, JPEG, BMP, TGA with quality options - AudioExportDialog: WAV, MP3, OGG with FFmpeg integration - BinaryExportDialog: Raw data export with optional decompression - BatchExportDialog: Recursive export with filtering options - Base ExportDialog class for common functionality Settings additions: - FFmpeg path configuration with auto-detection - Search common install locations and PATH Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
765 lines
25 KiB
C++
765 lines
25 KiB
C++
#include "exportmanager.h"
|
|
#include "settings.h"
|
|
#include "imageexportdialog.h"
|
|
#include "audioexportdialog.h"
|
|
#include "binaryexportdialog.h"
|
|
#include "batchexportdialog.h"
|
|
#include "../libs/dsl/dslkeys.h"
|
|
|
|
#include <QFileDialog>
|
|
#include <QMessageBox>
|
|
#include <QClipboard>
|
|
#include <QApplication>
|
|
#include <QMimeData>
|
|
#include <QProcess>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QStandardPaths>
|
|
#include <QTemporaryFile>
|
|
#include <QImageReader>
|
|
|
|
ExportManager& ExportManager::instance() {
|
|
static ExportManager instance;
|
|
return instance;
|
|
}
|
|
|
|
ExportManager::ExportManager(QObject* parent)
|
|
: QObject(parent)
|
|
{
|
|
}
|
|
|
|
ExportManager::ContentType ExportManager::detectContentType(const QVariantMap& vars) const {
|
|
// Check for viewer type in vars
|
|
if (DslKeys::contains(vars, DslKey::Viewer)) {
|
|
QString viewer = DslKeys::get(vars, DslKey::Viewer).toString();
|
|
if (viewer == "image") return Image;
|
|
if (viewer == "audio") return Audio;
|
|
if (viewer == "text") return Text;
|
|
if (viewer == "hex") return Binary;
|
|
}
|
|
return Unknown;
|
|
}
|
|
|
|
ExportManager::ContentType ExportManager::detectContentType(const QByteArray& data, const QString& filename) const {
|
|
QString ext = QFileInfo(filename).suffix().toLower();
|
|
|
|
// Check by extension
|
|
QStringList imageExts = {"png", "jpg", "jpeg", "bmp", "tga", "dds", "tiff", "tif", "webp", "gif"};
|
|
QStringList audioExts = {"wav", "mp3", "ogg", "flac", "aiff", "wma", "m4a"};
|
|
QStringList textExts = {"txt", "xml", "json", "csv", "ini", "cfg", "log", "md", "htm", "html"};
|
|
|
|
if (imageExts.contains(ext)) return Image;
|
|
if (audioExts.contains(ext)) return Audio;
|
|
if (textExts.contains(ext)) return Text;
|
|
|
|
// Check by magic bytes
|
|
if (data.size() >= 4) {
|
|
// PNG
|
|
if (data.startsWith("\x89PNG")) return Image;
|
|
// JPEG
|
|
if (data.startsWith("\xFF\xD8\xFF")) return Image;
|
|
// BMP
|
|
if (data.startsWith("BM")) return Image;
|
|
// GIF
|
|
if (data.startsWith("GIF8")) return Image;
|
|
// RIFF (WAV)
|
|
if (data.startsWith("RIFF") && data.size() >= 12 && data.mid(8, 4) == "WAVE") return Audio;
|
|
// OGG
|
|
if (data.startsWith("OggS")) return Audio;
|
|
// FLAC
|
|
if (data.startsWith("fLaC")) return Audio;
|
|
// ID3 (MP3)
|
|
if (data.startsWith("ID3") || (data.size() >= 2 && (uchar)data[0] == 0xFF && ((uchar)data[1] & 0xE0) == 0xE0)) return Audio;
|
|
}
|
|
|
|
// Check if it looks like text
|
|
bool looksLikeText = true;
|
|
int checkLen = qMin(data.size(), 1024);
|
|
for (int i = 0; i < checkLen && looksLikeText; ++i) {
|
|
char c = data[i];
|
|
if (c < 0x09 || (c > 0x0D && c < 0x20 && c != 0x1B)) {
|
|
looksLikeText = false;
|
|
}
|
|
}
|
|
if (looksLikeText && !data.isEmpty()) return Text;
|
|
|
|
return Binary;
|
|
}
|
|
|
|
bool ExportManager::exportRawData(const QByteArray& data, const QString& suggestedName, QWidget* parent) {
|
|
QString baseName = QFileInfo(suggestedName).baseName();
|
|
if (baseName.isEmpty()) baseName = "export";
|
|
|
|
QString dir = lastExportDir("raw");
|
|
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
|
|
|
QString savePath = QFileDialog::getSaveFileName(parent, "Export Raw Data",
|
|
dir + "/" + baseName + ".bin",
|
|
"Binary Files (*.bin *.dat);;All Files (*)");
|
|
|
|
if (savePath.isEmpty()) return false;
|
|
|
|
setLastExportDir("raw", QFileInfo(savePath).path());
|
|
|
|
QFile file(savePath);
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
emit exportError(QString("Failed to open file for writing: %1").arg(file.errorString()));
|
|
return false;
|
|
}
|
|
|
|
file.write(data);
|
|
file.close();
|
|
|
|
emit exportCompleted(savePath, true);
|
|
return true;
|
|
}
|
|
|
|
bool ExportManager::exportImage(const QImage& image, const QString& format, const QString& suggestedName, QWidget* parent) {
|
|
if (image.isNull()) {
|
|
emit exportError("Cannot export null image");
|
|
return false;
|
|
}
|
|
|
|
QString ext = format.toLower();
|
|
QString filter = getFilterForFormat(ext);
|
|
QString baseName = QFileInfo(suggestedName).baseName();
|
|
if (baseName.isEmpty()) baseName = "image";
|
|
|
|
QString dir = lastExportDir("image");
|
|
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
|
|
|
|
QString savePath = QFileDialog::getSaveFileName(parent, "Export Image",
|
|
dir + "/" + baseName + "." + ext, filter);
|
|
|
|
if (savePath.isEmpty()) return false;
|
|
|
|
setLastExportDir("image", QFileInfo(savePath).path());
|
|
|
|
bool success = false;
|
|
if (ext == "tga") {
|
|
success = saveTGA(image, savePath);
|
|
} else {
|
|
// Qt handles png, jpg, bmp, tiff natively
|
|
success = image.save(savePath, ext.toUpper().toUtf8().constData());
|
|
}
|
|
|
|
if (!success) {
|
|
emit exportError(QString("Failed to save image as %1").arg(ext.toUpper()));
|
|
return false;
|
|
}
|
|
|
|
emit exportCompleted(savePath, true);
|
|
return true;
|
|
}
|
|
|
|
bool ExportManager::exportAudio(const QByteArray& wavData, const QString& format, const QString& suggestedName, QWidget* parent) {
|
|
if (wavData.isEmpty()) {
|
|
emit exportError("No audio data to export");
|
|
return false;
|
|
}
|
|
|
|
QString ext = format.toLower();
|
|
QString baseName = QFileInfo(suggestedName).baseName();
|
|
if (baseName.isEmpty()) baseName = "audio";
|
|
|
|
QString dir = lastExportDir("audio");
|
|
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
|
|
|
|
QString filter;
|
|
if (ext == "wav") filter = "WAV Files (*.wav)";
|
|
else if (ext == "mp3") filter = "MP3 Files (*.mp3)";
|
|
else if (ext == "ogg") filter = "OGG Files (*.ogg)";
|
|
else if (ext == "flac") filter = "FLAC Files (*.flac)";
|
|
else filter = "All Files (*)";
|
|
|
|
QString savePath = QFileDialog::getSaveFileName(parent, "Export Audio",
|
|
dir + "/" + baseName + "." + ext, filter);
|
|
|
|
if (savePath.isEmpty()) return false;
|
|
|
|
setLastExportDir("audio", QFileInfo(savePath).path());
|
|
|
|
if (ext == "wav") {
|
|
// Direct write for WAV
|
|
QFile file(savePath);
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
emit exportError(QString("Failed to open file: %1").arg(file.errorString()));
|
|
return false;
|
|
}
|
|
file.write(wavData);
|
|
file.close();
|
|
emit exportCompleted(savePath, true);
|
|
return true;
|
|
}
|
|
|
|
// Need FFmpeg for MP3/OGG/FLAC conversion
|
|
if (!hasFFmpeg()) {
|
|
QMessageBox::warning(parent, "FFmpeg Required",
|
|
QString("FFmpeg is required to export audio as %1.\n\n"
|
|
"Install FFmpeg and ensure it's in your system PATH,\n"
|
|
"or configure the path in Edit > Preferences > Tools.").arg(ext.toUpper()));
|
|
return false;
|
|
}
|
|
|
|
// Write temp WAV file
|
|
QTemporaryFile tempWav;
|
|
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
|
|
if (!tempWav.open()) {
|
|
emit exportError("Failed to create temporary file");
|
|
return false;
|
|
}
|
|
tempWav.write(wavData);
|
|
QString tempPath = tempWav.fileName();
|
|
tempWav.close();
|
|
|
|
bool success = convertWithFFmpeg(tempPath, savePath, ext);
|
|
|
|
// Cleanup temp file
|
|
QFile::remove(tempPath);
|
|
|
|
if (success) {
|
|
emit exportCompleted(savePath, true);
|
|
}
|
|
return success;
|
|
}
|
|
|
|
bool ExportManager::exportText(const QByteArray& data, const QString& suggestedName, QWidget* parent) {
|
|
QString baseName = QFileInfo(suggestedName).baseName();
|
|
if (baseName.isEmpty()) baseName = "text";
|
|
|
|
QString dir = lastExportDir("text");
|
|
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
|
|
|
QString savePath = QFileDialog::getSaveFileName(parent, "Export Text",
|
|
dir + "/" + baseName + ".txt",
|
|
"Text Files (*.txt);;All Files (*)");
|
|
|
|
if (savePath.isEmpty()) return false;
|
|
|
|
setLastExportDir("text", QFileInfo(savePath).path());
|
|
|
|
QFile file(savePath);
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
|
emit exportError(QString("Failed to open file: %1").arg(file.errorString()));
|
|
return false;
|
|
}
|
|
|
|
file.write(data);
|
|
file.close();
|
|
|
|
emit exportCompleted(savePath, true);
|
|
return true;
|
|
}
|
|
|
|
bool ExportManager::exportWithDialog(const QByteArray& data, const QString& name,
|
|
ContentType type, QWidget* parent) {
|
|
switch (type) {
|
|
case Image: {
|
|
QImage image;
|
|
if (image.loadFromData(data)) {
|
|
return exportImageWithDialog(image, name, parent);
|
|
}
|
|
// Fall back to binary export
|
|
break;
|
|
}
|
|
case Audio: {
|
|
AudioExportDialog dialog(parent);
|
|
dialog.setData(data, name);
|
|
if (dialog.exec() == QDialog::Accepted) {
|
|
QString format = dialog.selectedFormat();
|
|
QString path = dialog.outputPath();
|
|
|
|
if (dialog.rememberSettings()) {
|
|
Settings::instance().setDefaultAudioExportFormat(format);
|
|
Settings::instance().setAudioMp3Bitrate(dialog.mp3Bitrate());
|
|
Settings::instance().setAudioOggQuality(dialog.oggQuality());
|
|
Settings::instance().setAudioFlacCompression(dialog.flacCompression());
|
|
}
|
|
|
|
// Export based on format
|
|
if (format == "wav") {
|
|
QFile file(path);
|
|
if (file.open(QIODevice::WriteOnly)) {
|
|
file.write(data);
|
|
file.close();
|
|
emit exportCompleted(path, true);
|
|
return true;
|
|
}
|
|
} else {
|
|
// Need FFmpeg conversion
|
|
QTemporaryFile tempWav;
|
|
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
|
|
if (tempWav.open()) {
|
|
tempWav.write(data);
|
|
QString tempPath = tempWav.fileName();
|
|
tempWav.close();
|
|
bool success = convertWithFFmpeg(tempPath, path, format);
|
|
QFile::remove(tempPath);
|
|
if (success) {
|
|
emit exportCompleted(path, true);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
case Text:
|
|
return exportText(data, name, parent);
|
|
|
|
case Binary:
|
|
case Unknown:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Binary export dialog
|
|
BinaryExportDialog dialog(parent);
|
|
dialog.setData(data, name);
|
|
if (dialog.exec() == QDialog::Accepted) {
|
|
QString path = dialog.outputPath();
|
|
QFile file(path);
|
|
if (file.open(QIODevice::WriteOnly)) {
|
|
file.write(data);
|
|
file.close();
|
|
emit exportCompleted(path, true);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ExportManager::exportImageWithDialog(const QImage& image, const QString& name, QWidget* parent) {
|
|
if (image.isNull()) {
|
|
emit exportError("Cannot export null image");
|
|
return false;
|
|
}
|
|
|
|
ImageExportDialog dialog(parent);
|
|
dialog.setImage(image, name);
|
|
|
|
if (dialog.exec() == QDialog::Accepted) {
|
|
QString format = dialog.selectedFormat();
|
|
QString path = dialog.outputPath();
|
|
|
|
if (dialog.rememberSettings()) {
|
|
Settings::instance().setDefaultImageExportFormat(format);
|
|
Settings::instance().setImageJpegQuality(dialog.jpegQuality());
|
|
Settings::instance().setImagePngCompression(dialog.pngCompression());
|
|
}
|
|
|
|
setLastExportDir("image", QFileInfo(path).path());
|
|
|
|
bool success = false;
|
|
if (format == "tga") {
|
|
success = saveTGA(image, path);
|
|
} else if (format == "jpg" || format == "jpeg") {
|
|
success = image.save(path, "JPEG", dialog.jpegQuality());
|
|
} else if (format == "png") {
|
|
// Qt doesn't support PNG compression level directly in save()
|
|
// We'd need to use QImageWriter for that
|
|
success = image.save(path, "PNG");
|
|
} else {
|
|
success = image.save(path, format.toUpper().toUtf8().constData());
|
|
}
|
|
|
|
if (success) {
|
|
emit exportCompleted(path, true);
|
|
return true;
|
|
} else {
|
|
emit exportError(QString("Failed to save image as %1").arg(format.toUpper()));
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ExportManager::quickExport(const QByteArray& data, const QString& name,
|
|
ContentType type, QWidget* parent) {
|
|
QString baseName = QFileInfo(name).baseName();
|
|
if (baseName.isEmpty()) baseName = "export";
|
|
|
|
QString dir, format, filter;
|
|
|
|
switch (type) {
|
|
case Image:
|
|
dir = lastExportDir("image");
|
|
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
|
|
format = Settings::instance().defaultImageExportFormat();
|
|
filter = getFilterForFormat(format);
|
|
break;
|
|
case Audio:
|
|
dir = lastExportDir("audio");
|
|
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
|
|
format = Settings::instance().defaultAudioExportFormat();
|
|
filter = QString("%1 Files (*.%2)").arg(format.toUpper()).arg(format);
|
|
break;
|
|
case Text:
|
|
dir = lastExportDir("text");
|
|
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
|
format = "txt";
|
|
filter = "Text Files (*.txt)";
|
|
break;
|
|
default:
|
|
dir = lastExportDir("raw");
|
|
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
|
format = "bin";
|
|
filter = "Binary Files (*.bin)";
|
|
break;
|
|
}
|
|
|
|
QString savePath = QFileDialog::getSaveFileName(parent, "Quick Export",
|
|
dir + "/" + baseName + "." + format, filter);
|
|
|
|
if (savePath.isEmpty()) return false;
|
|
|
|
// Handle type-specific export
|
|
switch (type) {
|
|
case Image: {
|
|
QImage image;
|
|
if (image.loadFromData(data)) {
|
|
setLastExportDir("image", QFileInfo(savePath).path());
|
|
QString ext = QFileInfo(savePath).suffix().toLower();
|
|
if (ext == "tga") {
|
|
return saveTGA(image, savePath);
|
|
}
|
|
int quality = (ext == "jpg" || ext == "jpeg") ?
|
|
Settings::instance().imageJpegQuality() : -1;
|
|
if (image.save(savePath, ext.toUpper().toUtf8().constData(), quality)) {
|
|
emit exportCompleted(savePath, true);
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case Audio: {
|
|
setLastExportDir("audio", QFileInfo(savePath).path());
|
|
QString ext = QFileInfo(savePath).suffix().toLower();
|
|
if (ext == "wav") {
|
|
QFile file(savePath);
|
|
if (file.open(QIODevice::WriteOnly)) {
|
|
file.write(data);
|
|
file.close();
|
|
emit exportCompleted(savePath, true);
|
|
return true;
|
|
}
|
|
} else {
|
|
// FFmpeg conversion
|
|
QTemporaryFile tempWav;
|
|
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
|
|
if (tempWav.open()) {
|
|
tempWav.write(data);
|
|
QString tempPath = tempWav.fileName();
|
|
tempWav.close();
|
|
bool success = convertWithFFmpeg(tempPath, savePath, ext);
|
|
QFile::remove(tempPath);
|
|
if (success) {
|
|
emit exportCompleted(savePath, true);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
setLastExportDir("raw", QFileInfo(savePath).path());
|
|
QFile file(savePath);
|
|
if (file.open(QIODevice::WriteOnly)) {
|
|
file.write(data);
|
|
file.close();
|
|
emit exportCompleted(savePath, true);
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ExportManager::quickExportImage(const QImage& image, const QString& name, QWidget* parent) {
|
|
if (image.isNull()) return false;
|
|
|
|
QString baseName = QFileInfo(name).baseName();
|
|
if (baseName.isEmpty()) baseName = "image";
|
|
|
|
QString dir = lastExportDir("image");
|
|
if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
|
|
|
|
QString format = Settings::instance().defaultImageExportFormat();
|
|
QString filter = getFilterForFormat(format);
|
|
|
|
QString savePath = QFileDialog::getSaveFileName(parent, "Quick Export Image",
|
|
dir + "/" + baseName + "." + format, filter);
|
|
|
|
if (savePath.isEmpty()) return false;
|
|
|
|
setLastExportDir("image", QFileInfo(savePath).path());
|
|
|
|
QString ext = QFileInfo(savePath).suffix().toLower();
|
|
if (ext == "tga") {
|
|
if (saveTGA(image, savePath)) {
|
|
emit exportCompleted(savePath, true);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int quality = (ext == "jpg" || ext == "jpeg") ?
|
|
Settings::instance().imageJpegQuality() : -1;
|
|
|
|
if (image.save(savePath, ext.toUpper().toUtf8().constData(), quality)) {
|
|
emit exportCompleted(savePath, true);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ExportManager::batchExport(const QList<BatchExportItem>& items, QWidget* parent) {
|
|
if (items.isEmpty()) return false;
|
|
|
|
BatchExportDialog dialog(parent);
|
|
dialog.setItems(items);
|
|
|
|
if (dialog.exec() != QDialog::Accepted) return false;
|
|
|
|
QList<BatchExportItem> selected = dialog.selectedItems();
|
|
if (selected.isEmpty()) return false;
|
|
|
|
QString outputDir = dialog.outputDirectory();
|
|
bool preserveStructure = dialog.preserveStructure();
|
|
QString conflictResolution = dialog.conflictResolution();
|
|
QString imageFormat = dialog.imageFormat();
|
|
QString audioFormat = dialog.audioFormat();
|
|
|
|
int succeeded = 0, failed = 0, skipped = 0;
|
|
|
|
for (int i = 0; i < selected.size(); ++i) {
|
|
const BatchExportItem& item = selected[i];
|
|
emit batchExportProgress(i + 1, selected.size(), item.name);
|
|
|
|
// Determine output path
|
|
QString relativePath = preserveStructure ? item.path : item.name;
|
|
QString ext;
|
|
|
|
switch (item.contentType) {
|
|
case Image: ext = imageFormat; break;
|
|
case Audio: ext = audioFormat; break;
|
|
case Text: ext = "txt"; break;
|
|
default: ext = "bin"; break;
|
|
}
|
|
|
|
QString baseName = QFileInfo(relativePath).baseName();
|
|
QString subDir = preserveStructure ? QFileInfo(relativePath).path() : "";
|
|
QString targetDir = subDir.isEmpty() ? outputDir : outputDir + "/" + subDir;
|
|
QString targetPath = targetDir + "/" + baseName + "." + ext;
|
|
|
|
// Ensure directory exists
|
|
QDir().mkpath(targetDir);
|
|
|
|
// Handle conflicts
|
|
if (QFile::exists(targetPath)) {
|
|
if (conflictResolution == "skip") {
|
|
skipped++;
|
|
continue;
|
|
} else if (conflictResolution == "number") {
|
|
int num = 1;
|
|
QString newPath;
|
|
do {
|
|
newPath = targetDir + "/" + baseName + QString("_%1.").arg(num++) + ext;
|
|
} while (QFile::exists(newPath) && num < 1000);
|
|
targetPath = newPath;
|
|
}
|
|
// "overwrite" - just proceed
|
|
}
|
|
|
|
// Export based on type
|
|
bool success = false;
|
|
switch (item.contentType) {
|
|
case Image: {
|
|
QImage image;
|
|
if (image.loadFromData(item.data)) {
|
|
if (ext == "tga") {
|
|
success = saveTGA(image, targetPath);
|
|
} else {
|
|
int quality = (ext == "jpg") ? Settings::instance().imageJpegQuality() : -1;
|
|
success = image.save(targetPath, ext.toUpper().toUtf8().constData(), quality);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case Audio:
|
|
if (ext == "wav") {
|
|
QFile file(targetPath);
|
|
if (file.open(QIODevice::WriteOnly)) {
|
|
file.write(item.data);
|
|
file.close();
|
|
success = true;
|
|
}
|
|
} else {
|
|
QTemporaryFile tempWav;
|
|
tempWav.setFileTemplate(QDir::temp().filePath("xplor_XXXXXX.wav"));
|
|
if (tempWav.open()) {
|
|
tempWav.write(item.data);
|
|
QString tempPath = tempWav.fileName();
|
|
tempWav.close();
|
|
success = convertWithFFmpeg(tempPath, targetPath, ext);
|
|
QFile::remove(tempPath);
|
|
}
|
|
}
|
|
break;
|
|
default: {
|
|
QFile file(targetPath);
|
|
if (file.open(QIODevice::WriteOnly)) {
|
|
file.write(item.data);
|
|
file.close();
|
|
success = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (success) {
|
|
succeeded++;
|
|
} else {
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
emit batchExportCompleted(succeeded, failed, skipped);
|
|
return failed == 0;
|
|
}
|
|
|
|
QStringList ExportManager::supportedImageFormats() const {
|
|
return {"png", "jpg", "bmp", "tiff", "tga"};
|
|
}
|
|
|
|
QStringList ExportManager::supportedAudioFormats() const {
|
|
return {"wav", "mp3", "ogg", "flac"};
|
|
}
|
|
|
|
bool ExportManager::hasFFmpeg() const {
|
|
if (!m_ffmpegChecked) {
|
|
m_cachedFFmpegPath = findFFmpegPath();
|
|
m_ffmpegChecked = true;
|
|
}
|
|
return !m_cachedFFmpegPath.isEmpty();
|
|
}
|
|
|
|
void ExportManager::copyImageToClipboard(const QImage& image) {
|
|
if (image.isNull()) return;
|
|
QApplication::clipboard()->setImage(image);
|
|
}
|
|
|
|
void ExportManager::copyTextToClipboard(const QString& text) {
|
|
QApplication::clipboard()->setText(text);
|
|
}
|
|
|
|
void ExportManager::copyBinaryAsHex(const QByteArray& data) {
|
|
QString hex = data.toHex(' ').toUpper();
|
|
QApplication::clipboard()->setText(hex);
|
|
}
|
|
|
|
QString ExportManager::lastExportDir(const QString& category) const {
|
|
return m_lastDirs.value(category);
|
|
}
|
|
|
|
void ExportManager::setLastExportDir(const QString& category, const QString& dir) {
|
|
m_lastDirs[category] = dir;
|
|
}
|
|
|
|
bool ExportManager::saveTGA(const QImage& image, const QString& path) {
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::WriteOnly)) return false;
|
|
|
|
QImage img = image.convertToFormat(QImage::Format_ARGB32);
|
|
|
|
// TGA Header (18 bytes)
|
|
QByteArray header(18, 0);
|
|
header[2] = 2; // Uncompressed true-color
|
|
header[12] = img.width() & 0xFF;
|
|
header[13] = (img.width() >> 8) & 0xFF;
|
|
header[14] = img.height() & 0xFF;
|
|
header[15] = (img.height() >> 8) & 0xFF;
|
|
header[16] = 32; // 32 bits per pixel
|
|
header[17] = 0x20; // Top-left origin
|
|
|
|
file.write(header);
|
|
|
|
// Write BGRA pixels (TGA uses BGRA order)
|
|
for (int y = 0; y < img.height(); ++y) {
|
|
for (int x = 0; x < img.width(); ++x) {
|
|
QRgb pixel = img.pixel(x, y);
|
|
char bgra[4] = {
|
|
static_cast<char>(qBlue(pixel)),
|
|
static_cast<char>(qGreen(pixel)),
|
|
static_cast<char>(qRed(pixel)),
|
|
static_cast<char>(qAlpha(pixel))
|
|
};
|
|
file.write(bgra, 4);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QString ExportManager::getFilterForFormat(const QString& format) const {
|
|
QString ext = format.toLower();
|
|
if (ext == "png") return "PNG Images (*.png)";
|
|
if (ext == "jpg" || ext == "jpeg") return "JPEG Images (*.jpg *.jpeg)";
|
|
if (ext == "bmp") return "BMP Images (*.bmp)";
|
|
if (ext == "tiff" || ext == "tif") return "TIFF Images (*.tiff *.tif)";
|
|
if (ext == "tga") return "TGA Images (*.tga)";
|
|
if (ext == "webp") return "WebP Images (*.webp)";
|
|
return "All Files (*)";
|
|
}
|
|
|
|
bool ExportManager::convertWithFFmpeg(const QString& inputPath, const QString& outputPath, const QString& format) {
|
|
QString ffmpegPath = m_cachedFFmpegPath;
|
|
if (ffmpegPath.isEmpty()) {
|
|
ffmpegPath = findFFmpegPath();
|
|
}
|
|
|
|
if (ffmpegPath.isEmpty()) {
|
|
emit exportError("FFmpeg not found");
|
|
return false;
|
|
}
|
|
|
|
QProcess ffmpeg;
|
|
QStringList args;
|
|
args << "-y"; // Overwrite output
|
|
args << "-i" << inputPath;
|
|
|
|
// Format-specific encoding settings
|
|
if (format == "mp3") {
|
|
args << "-codec:a" << "libmp3lame" << "-qscale:a" << "2";
|
|
} else if (format == "ogg") {
|
|
args << "-codec:a" << "libvorbis" << "-qscale:a" << "5";
|
|
} else if (format == "flac") {
|
|
args << "-codec:a" << "flac";
|
|
}
|
|
|
|
args << outputPath;
|
|
|
|
ffmpeg.start(ffmpegPath, args);
|
|
|
|
if (!ffmpeg.waitForFinished(30000)) {
|
|
emit exportError("FFmpeg conversion timed out");
|
|
return false;
|
|
}
|
|
|
|
if (ffmpeg.exitCode() != 0) {
|
|
QString errorOutput = QString::fromUtf8(ffmpeg.readAllStandardError());
|
|
emit exportError(QString("FFmpeg conversion failed: %1").arg(errorOutput.left(200)));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QString ExportManager::findFFmpegPath() const {
|
|
// Use Settings' ffmpegPath which handles auto-detection
|
|
return Settings::instance().ffmpegPath();
|
|
}
|