XPlor/app/imageexportdialog.cpp
njohnson d7488c5fa9 Add comprehensive export system with format-specific dialogs
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>
2026-01-12 20:54:38 -05:00

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;
}
}
}