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>
283 lines
9.7 KiB
C++
283 lines
9.7 KiB
C++
#include "imageexportdialog.h"
|
|
#include "settings.h"
|
|
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QSlider>
|
|
#include <QSpinBox>
|
|
#include <QComboBox>
|
|
#include <QStackedWidget>
|
|
#include <QGroupBox>
|
|
#include <QFileInfo>
|
|
|
|
ImageExportDialog::ImageExportDialog(QWidget *parent)
|
|
: ExportDialog(ContentType::Image, parent)
|
|
, mPreviewLabel(nullptr)
|
|
, mOptionsStack(nullptr)
|
|
, mJpegOptionsWidget(nullptr)
|
|
, mJpegQualitySlider(nullptr)
|
|
, mJpegQualitySpinBox(nullptr)
|
|
, mPngOptionsWidget(nullptr)
|
|
, mPngCompressionSlider(nullptr)
|
|
, mPngCompressionSpinBox(nullptr)
|
|
, mNoOptionsWidget(nullptr)
|
|
{
|
|
// Populate format combo
|
|
for (const QString& fmt : supportedFormats()) {
|
|
formatCombo()->addItem(fmt.toUpper());
|
|
}
|
|
|
|
// Set default format from settings
|
|
QString defaultFormat = Settings::instance().defaultImageExportFormat().toUpper();
|
|
int index = formatCombo()->findText(defaultFormat);
|
|
if (index >= 0) {
|
|
formatCombo()->setCurrentIndex(index);
|
|
}
|
|
|
|
setupPreview();
|
|
}
|
|
|
|
void ImageExportDialog::setupPreview()
|
|
{
|
|
// Create preview label inside preview container
|
|
QVBoxLayout* previewLayout = new QVBoxLayout(previewContainer());
|
|
previewLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
mPreviewLabel = new QLabel(previewContainer());
|
|
mPreviewLabel->setAlignment(Qt::AlignCenter);
|
|
mPreviewLabel->setMinimumSize(256, 256);
|
|
mPreviewLabel->setText("No image loaded");
|
|
mPreviewLabel->setStyleSheet("color: #808080;");
|
|
previewLayout->addWidget(mPreviewLabel);
|
|
|
|
// Create stacked widget for format-specific options
|
|
mOptionsStack = new QStackedWidget(this);
|
|
|
|
// JPEG options
|
|
mJpegOptionsWidget = new QWidget(this);
|
|
QVBoxLayout* jpegLayout = new QVBoxLayout(mJpegOptionsWidget);
|
|
jpegLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
QLabel* jpegQualityLabel = new QLabel("Quality:", mJpegOptionsWidget);
|
|
QHBoxLayout* jpegSliderLayout = new QHBoxLayout();
|
|
mJpegQualitySlider = new QSlider(Qt::Horizontal, mJpegOptionsWidget);
|
|
mJpegQualitySlider->setRange(1, 100);
|
|
mJpegQualitySlider->setValue(Settings::instance().imageJpegQuality());
|
|
mJpegQualitySpinBox = new QSpinBox(mJpegOptionsWidget);
|
|
mJpegQualitySpinBox->setRange(1, 100);
|
|
mJpegQualitySpinBox->setValue(Settings::instance().imageJpegQuality());
|
|
jpegSliderLayout->addWidget(mJpegQualitySlider);
|
|
jpegSliderLayout->addWidget(mJpegQualitySpinBox);
|
|
|
|
jpegLayout->addWidget(jpegQualityLabel);
|
|
jpegLayout->addLayout(jpegSliderLayout);
|
|
jpegLayout->addStretch();
|
|
|
|
connect(mJpegQualitySlider, &QSlider::valueChanged, this, &ImageExportDialog::onJpegQualityChanged);
|
|
connect(mJpegQualitySpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
|
|
mJpegQualitySlider, &QSlider::setValue);
|
|
|
|
// PNG options
|
|
mPngOptionsWidget = new QWidget(this);
|
|
QVBoxLayout* pngLayout = new QVBoxLayout(mPngOptionsWidget);
|
|
pngLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
QLabel* pngCompressionLabel = new QLabel("Compression (0=fast, 9=best):", mPngOptionsWidget);
|
|
QHBoxLayout* pngSliderLayout = new QHBoxLayout();
|
|
mPngCompressionSlider = new QSlider(Qt::Horizontal, mPngOptionsWidget);
|
|
mPngCompressionSlider->setRange(0, 9);
|
|
mPngCompressionSlider->setValue(Settings::instance().imagePngCompression());
|
|
mPngCompressionSpinBox = new QSpinBox(mPngOptionsWidget);
|
|
mPngCompressionSpinBox->setRange(0, 9);
|
|
mPngCompressionSpinBox->setValue(Settings::instance().imagePngCompression());
|
|
pngSliderLayout->addWidget(mPngCompressionSlider);
|
|
pngSliderLayout->addWidget(mPngCompressionSpinBox);
|
|
|
|
pngLayout->addWidget(pngCompressionLabel);
|
|
pngLayout->addLayout(pngSliderLayout);
|
|
pngLayout->addStretch();
|
|
|
|
connect(mPngCompressionSlider, &QSlider::valueChanged, this, &ImageExportDialog::onPngCompressionChanged);
|
|
connect(mPngCompressionSpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
|
|
mPngCompressionSlider, &QSlider::setValue);
|
|
|
|
// No options widget (for formats without settings)
|
|
mNoOptionsWidget = new QWidget(this);
|
|
QVBoxLayout* noOptionsLayout = new QVBoxLayout(mNoOptionsWidget);
|
|
noOptionsLayout->setContentsMargins(0, 0, 0, 0);
|
|
QLabel* noOptionsLabel = new QLabel("No additional options for this format.", mNoOptionsWidget);
|
|
noOptionsLabel->setStyleSheet("color: #808080;");
|
|
noOptionsLayout->addWidget(noOptionsLabel);
|
|
noOptionsLayout->addStretch();
|
|
|
|
// Add to stacked widget
|
|
mOptionsStack->addWidget(mJpegOptionsWidget); // Index 0
|
|
mOptionsStack->addWidget(mPngOptionsWidget); // Index 1
|
|
mOptionsStack->addWidget(mNoOptionsWidget); // Index 2
|
|
|
|
// Add stacked widget to options container
|
|
QVBoxLayout* optionsLayout = qobject_cast<QVBoxLayout*>(optionsContainer()->layout());
|
|
if (optionsLayout) {
|
|
optionsLayout->addWidget(mOptionsStack);
|
|
}
|
|
|
|
// Show options for current format
|
|
showOptionsForFormat(formatCombo()->currentText());
|
|
}
|
|
|
|
void ImageExportDialog::updatePreview()
|
|
{
|
|
if (mImage.isNull()) {
|
|
mPreviewLabel->setText("No image loaded");
|
|
return;
|
|
}
|
|
|
|
// Scale image to fit preview area while maintaining aspect ratio
|
|
QSize previewSize = mPreviewLabel->size();
|
|
QPixmap pixmap = QPixmap::fromImage(mImage.scaled(
|
|
previewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
|
mPreviewLabel->setPixmap(pixmap);
|
|
|
|
updateFileSizeEstimate();
|
|
}
|
|
|
|
QStringList ImageExportDialog::supportedFormats() const
|
|
{
|
|
return {"png", "jpg", "bmp", "tiff", "tga"};
|
|
}
|
|
|
|
void ImageExportDialog::setImage(const QImage& image, const QString& suggestedName)
|
|
{
|
|
mImage = image;
|
|
mSuggestedName = suggestedName;
|
|
|
|
// Update info label with image details
|
|
QString info = QString("%1 x %2, %3-bit")
|
|
.arg(image.width())
|
|
.arg(image.height())
|
|
.arg(image.depth());
|
|
|
|
// Find the info label (it's in the base class)
|
|
QList<QLabel*> labels = findChildren<QLabel*>();
|
|
for (QLabel* label : labels) {
|
|
if (label->objectName().isEmpty() && label != mPreviewLabel) {
|
|
// This is likely the info label
|
|
label->setText(info);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Call base class to set output path
|
|
setData(QByteArray(), suggestedName);
|
|
|
|
updatePreview();
|
|
}
|
|
|
|
void ImageExportDialog::onFormatChanged(const QString& format)
|
|
{
|
|
ExportDialog::onFormatChanged(format);
|
|
showOptionsForFormat(format);
|
|
updateFileSizeEstimate();
|
|
}
|
|
|
|
void ImageExportDialog::showOptionsForFormat(const QString& format)
|
|
{
|
|
QString fmt = format.toLower();
|
|
if (fmt == "jpg" || fmt == "jpeg") {
|
|
mOptionsStack->setCurrentWidget(mJpegOptionsWidget);
|
|
} else if (fmt == "png") {
|
|
mOptionsStack->setCurrentWidget(mPngOptionsWidget);
|
|
} else {
|
|
mOptionsStack->setCurrentWidget(mNoOptionsWidget);
|
|
}
|
|
}
|
|
|
|
int ImageExportDialog::jpegQuality() const
|
|
{
|
|
return mJpegQualitySlider ? mJpegQualitySlider->value() : 90;
|
|
}
|
|
|
|
int ImageExportDialog::pngCompression() const
|
|
{
|
|
return mPngCompressionSlider ? mPngCompressionSlider->value() : 6;
|
|
}
|
|
|
|
void ImageExportDialog::onJpegQualityChanged(int value)
|
|
{
|
|
if (mJpegQualitySpinBox) {
|
|
mJpegQualitySpinBox->blockSignals(true);
|
|
mJpegQualitySpinBox->setValue(value);
|
|
mJpegQualitySpinBox->blockSignals(false);
|
|
}
|
|
updateFileSizeEstimate();
|
|
}
|
|
|
|
void ImageExportDialog::onPngCompressionChanged(int value)
|
|
{
|
|
if (mPngCompressionSpinBox) {
|
|
mPngCompressionSpinBox->blockSignals(true);
|
|
mPngCompressionSpinBox->setValue(value);
|
|
mPngCompressionSpinBox->blockSignals(false);
|
|
}
|
|
updateFileSizeEstimate();
|
|
}
|
|
|
|
void ImageExportDialog::updateFileSizeEstimate()
|
|
{
|
|
if (mImage.isNull()) return;
|
|
|
|
QString format = selectedFormat();
|
|
qint64 estimatedSize = 0;
|
|
|
|
// Rough size estimates
|
|
int pixelCount = mImage.width() * mImage.height();
|
|
|
|
if (format == "jpg" || format == "jpeg") {
|
|
// JPEG: roughly quality/100 * pixelCount * 0.3 bytes
|
|
double qualityFactor = jpegQuality() / 100.0;
|
|
estimatedSize = static_cast<qint64>(pixelCount * 0.3 * qualityFactor);
|
|
} else if (format == "png") {
|
|
// PNG: depends on compression and image content, rough estimate
|
|
double compressionFactor = 1.0 - (pngCompression() * 0.08);
|
|
estimatedSize = static_cast<qint64>(pixelCount * 3 * compressionFactor * 0.5);
|
|
} else if (format == "bmp") {
|
|
// BMP: uncompressed, 3-4 bytes per pixel + header
|
|
estimatedSize = pixelCount * 4 + 54;
|
|
} else if (format == "tga") {
|
|
// TGA: uncompressed, 4 bytes per pixel + header
|
|
estimatedSize = pixelCount * 4 + 18;
|
|
} else if (format == "tiff") {
|
|
// TIFF: similar to PNG
|
|
estimatedSize = static_cast<qint64>(pixelCount * 3 * 0.5);
|
|
}
|
|
|
|
// Format size string
|
|
QString sizeStr;
|
|
if (estimatedSize > 1024 * 1024) {
|
|
sizeStr = QString("Est: ~%1 MB").arg(estimatedSize / (1024.0 * 1024.0), 0, 'f', 1);
|
|
} else if (estimatedSize > 1024) {
|
|
sizeStr = QString("Est: ~%1 KB").arg(estimatedSize / 1024.0, 0, 'f', 0);
|
|
} else {
|
|
sizeStr = QString("Est: ~%1 bytes").arg(estimatedSize);
|
|
}
|
|
|
|
// Update info label
|
|
QString info = QString("%1 x %2, %3-bit\n%4")
|
|
.arg(mImage.width())
|
|
.arg(mImage.height())
|
|
.arg(mImage.depth())
|
|
.arg(sizeStr);
|
|
|
|
QList<QLabel*> labels = findChildren<QLabel*>();
|
|
for (QLabel* label : labels) {
|
|
if (label->objectName().isEmpty() && label != mPreviewLabel &&
|
|
!label->text().contains("Quality") && !label->text().contains("Compression") &&
|
|
!label->text().contains("Format") && !label->text().contains("Output") &&
|
|
!label->text().contains("No additional")) {
|
|
label->setText(info);
|
|
break;
|
|
}
|
|
}
|
|
}
|