14.18: Export As for PNG, JPEG and PDF, sized, over a checkerboard
This commit is contained in:
26 files changed
+1352
-688
No files matched your search
@@ -40,7 +40,9 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, Pro
|
||||
file->addSeparator();
|
||||
add(file, QStringLiteral("exportPNG"), QStringLiteral("Export PNG…"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_E), [this] { projects().exportPNG(); });
|
||||
add(file, QStringLiteral("exportJPEG"), QStringLiteral("Export JPEG…"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::SHIFT | Qt::Key_S),
|
||||
[this] { projects().exportJPEG(); });
|
||||
[this] { projects().exportAs(ExportFormat::jpeg); });
|
||||
// Swift 1.4.8: Photoshop's Ctrl+Alt+Shift+W.
|
||||
add(file, QStringLiteral("exportAs"), QStringLiteral("Export As…"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::SHIFT | Qt::Key_W), [this] { projects().exportAs(); });
|
||||
file->addSeparator();
|
||||
add(file, QStringLiteral("closeProject"), QStringLiteral("Close Project"), QKeySequence(Qt::CTRL | Qt::Key_W), [this] { projects().close(&m_window); });
|
||||
|
||||
@@ -356,6 +358,7 @@ void CompositorMenus::synchronize()
|
||||
action(QStringLiteral("saveAs"))->setEnabled(drawn && p.canStart());
|
||||
action(QStringLiteral("exportPNG"))->setEnabled(drawn && p.canStart());
|
||||
action(QStringLiteral("exportJPEG"))->setEnabled(drawn && p.canStart());
|
||||
action(QStringLiteral("exportAs"))->setEnabled(drawn && p.canStart());
|
||||
action(QStringLiteral("closeProject"))->setEnabled(p.canStart());
|
||||
// A step's ampersand, doubled: a lone one is a mnemonic.
|
||||
const auto shown = [](QString name) { return name.replace(QLatin1Char('&'), QStringLiteral("&&")); };
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
#include "UI/JPEGExportSheet.h"
|
||||
#include "UI/ExportAsSheet.h"
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QScrollBar>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// Swift's tile: 10-point squares of grays 0.30 and 0.35.
|
||||
QPixmap JPEGPreview::checkerboard(double ratio)
|
||||
{
|
||||
QPixmap tile(QSize(20, 20) * ratio);
|
||||
tile.setDevicePixelRatio(ratio);
|
||||
QPainter painter(&tile);
|
||||
painter.fillRect(QRectF(0, 0, 20, 20), QColor::fromRgbF(0.30f, 0.30f, 0.30f));
|
||||
painter.fillRect(QRectF(0, 0, 10, 10), QColor::fromRgbF(0.35f, 0.35f, 0.35f));
|
||||
painter.fillRect(QRectF(10, 10, 10, 10), QColor::fromRgbF(0.35f, 0.35f, 0.35f));
|
||||
return tile;
|
||||
}
|
||||
|
||||
double JPEGPreview::fitZoom(int width, int height, QSizeF frame, double displayScale)
|
||||
{
|
||||
const double scale = std::max(1.0, displayScale);
|
||||
@@ -21,8 +33,8 @@ std::optional<double> JPEGPreview::step(double zoom, int direction)
|
||||
return next == steps.rend() ? std::nullopt : std::optional(*next);
|
||||
}
|
||||
|
||||
JPEGPreview::JPEGPreview(QSize pixels, std::function<void()> zoomChanged, QWidget *parent)
|
||||
: QAbstractScrollArea(parent), m_pixels(pixels), m_zoomChanged(std::move(zoomChanged))
|
||||
JPEGPreview::JPEGPreview(std::function<void()> zoomChanged, QWidget *parent)
|
||||
: QAbstractScrollArea(parent), m_zoomChanged(std::move(zoomChanged))
|
||||
{
|
||||
setFixedSize(frame);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
@@ -31,13 +43,17 @@ JPEGPreview::JPEGPreview(QSize pixels, std::function<void()> zoomChanged, QWidge
|
||||
verticalScrollBar()->setSingleStep(20);
|
||||
}
|
||||
|
||||
void JPEGPreview::setImage(const QImage &image, bool updating)
|
||||
void JPEGPreview::setImage(const QImage &image, QSize pixels, bool updating)
|
||||
{
|
||||
// A new image only: the averaged copy still holds.
|
||||
if (image.cacheKey() != m_image.cacheKey())
|
||||
m_reduced = QImage();
|
||||
m_image = image;
|
||||
m_updating = updating;
|
||||
if (pixels != m_pixels) {
|
||||
m_pixels = pixels;
|
||||
updateRanges();
|
||||
}
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
@@ -118,6 +134,11 @@ void JPEGPreview::paintEvent(QPaintEvent *)
|
||||
target = QRectF(QPointF(), QSizeF(m_image.size()).scaled(view.size(), Qt::KeepAspectRatio));
|
||||
target.moveCenter(view.center());
|
||||
}
|
||||
// Swift 1.4.8: the canvas's checkerboard shows what is clear.
|
||||
painter.save();
|
||||
painter.setBrushOrigin(target.topLeft());
|
||||
painter.fillRect(target, QBrush(checkerboard(devicePixelRatioF())));
|
||||
painter.restore();
|
||||
// Nearest from 100% up: each JPEG pixel as it is.
|
||||
if (m_zoom && *m_zoom >= 1) {
|
||||
painter.drawImage(target, m_image);
|
||||
@@ -0,0 +1,431 @@
|
||||
#include "UI/ExportAsSheet.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "UI/ByteCounts.h"
|
||||
#include "UI/ColorPickerSheet+Dialog.h"
|
||||
#include "UI/LayerIcons.h"
|
||||
#include "Document/DocumentLimits.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include <QButtonGroup>
|
||||
#include <QFutureWatcher>
|
||||
#include <QGridLayout>
|
||||
#include <QMenu>
|
||||
#include <QToolButton>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QShortcut>
|
||||
#include <QSlider>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtConcurrent>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
const QString ExportAsSheet::qualityKey = QStringLiteral("jpegExportQuality");
|
||||
|
||||
namespace {
|
||||
QLabel *text(const QString &words, int pixels, QFont::Weight weight, QPalette::ColorRole role, QWidget *parent)
|
||||
{
|
||||
auto *label = new QLabel(words, parent);
|
||||
label->setTextFormat(Qt::PlainText);
|
||||
QFont font = label->font();
|
||||
font.setPixelSize(pixels);
|
||||
font.setWeight(weight);
|
||||
label->setFont(font);
|
||||
label->setForegroundRole(role);
|
||||
return label;
|
||||
}
|
||||
|
||||
// Swift reads a finite number alone, clamped to 0–1.
|
||||
double startQuality()
|
||||
{
|
||||
bool number = false;
|
||||
const double saved = QSettings().value(ExportAsSheet::qualityKey).toDouble(&number);
|
||||
return number && std::isfinite(saved) ? std::clamp(saved, 0.0, 1.0) : JPEGOptions().quality;
|
||||
}
|
||||
|
||||
// Swift's 0...2 decimals, trailing zeros dropped.
|
||||
QString inches(double value)
|
||||
{
|
||||
QString shown = QLocale(QLocale::English, QLocale::UnitedStates).toString(value, 'f', 2);
|
||||
while (shown.contains(QLatin1Char('.')) && (shown.endsWith(QLatin1Char('0')) || shown.endsWith(QLatin1Char('.'))))
|
||||
shown.chop(1);
|
||||
return shown;
|
||||
}
|
||||
|
||||
struct Made {
|
||||
std::optional<QByteArray> data;
|
||||
QImage preview;
|
||||
std::optional<QString> failure;
|
||||
};
|
||||
}
|
||||
|
||||
ExportAsSheet::ExportAsSheet(ExportRaster raster, EditorSession &session, ExportFormat format, std::function<void(std::optional<Chosen>)> finish, QWidget *parent)
|
||||
: QWidget(parent), m_raster(std::move(raster)), m_session(session), m_finish(std::move(finish)),
|
||||
m_settings{format, JPEGOptions(), m_raster.image.width(), m_raster.image.height()}, m_wait(new QTimer(this)),
|
||||
m_preview(new JPEGPreview([this] { synchronize(); }, this)), m_spinner(new QProgressBar(m_preview)), m_formats(new QButtonGroup(this)),
|
||||
m_width(new PickerField([this] {
|
||||
bool number = false;
|
||||
const int value = m_width->text().toInt(&number);
|
||||
if (number)
|
||||
setScale(double(value) / m_raster.image.width());
|
||||
// A number applies; other text goes back.
|
||||
showSize();
|
||||
synchronize();
|
||||
}, [](int) {}, this)),
|
||||
m_height(new PickerField([this] {
|
||||
bool number = false;
|
||||
const int value = m_height->text().toInt(&number);
|
||||
if (number)
|
||||
setScale(double(value) / m_raster.image.height());
|
||||
// A number applies; other text goes back.
|
||||
showSize();
|
||||
synchronize();
|
||||
}, [](int) {}, this)),
|
||||
m_scale(new QToolButton(this)), m_quality(new QSlider(Qt::Horizontal, this)), m_percent(text(QString(), 13, QFont::Normal, QPalette::WindowText, this)),
|
||||
m_page(text(QString(), 13, QFont::Normal, QPalette::PlaceholderText, this)),
|
||||
m_matteSwatch(new DialogColorSwatch(
|
||||
QStringLiteral("Export Background"), [this] { return PaletteColor{m_settings.options.red, m_settings.options.green, m_settings.options.blue}; },
|
||||
[this](const PaletteColor &matte) { setMatte(matte); }, session, this)),
|
||||
m_note(text(QString(), 13, QFont::Normal, QPalette::PlaceholderText, this)), m_failure(text(QString(), 13, QFont::Normal, QPalette::BrightText, this)),
|
||||
m_bytes(text(QString(), 13, QFont::Normal, QPalette::WindowText, this)),
|
||||
m_updating(text(QStringLiteral("Updating…"), 13, QFont::Normal, QPalette::PlaceholderText, this)),
|
||||
m_export(new QPushButton(QStringLiteral("Export…"), this)), m_fit(new QPushButton(QStringLiteral("Fit"), this)), m_zoomIn(new QPushButton(this)),
|
||||
m_zoomOut(new QPushButton(this))
|
||||
{
|
||||
m_settings.options.quality = startQuality();
|
||||
auto *column = new QVBoxLayout(this);
|
||||
column->setContentsMargins(24, 24, 24, 24);
|
||||
column->setSpacing(16);
|
||||
// The title row sits 8 above; it inherits 8.
|
||||
auto *top = new QVBoxLayout;
|
||||
top->setSpacing(8);
|
||||
auto *title = new QHBoxLayout;
|
||||
title->addWidget(text(QStringLiteral("Export As"), 17, QFont::Bold, QPalette::WindowText, this));
|
||||
title->addStretch(1);
|
||||
m_fit->setObjectName(QStringLiteral("exportFit"));
|
||||
m_zoomIn->setObjectName(QStringLiteral("exportZoomIn"));
|
||||
m_zoomIn->setAccessibleName(QStringLiteral("Zoom in"));
|
||||
m_zoomOut->setObjectName(QStringLiteral("exportZoomOut"));
|
||||
m_zoomOut->setAccessibleName(QStringLiteral("Zoom out"));
|
||||
for (QPushButton *button : {m_fit, m_zoomIn, m_zoomOut}) {
|
||||
button->setAutoDefault(false);
|
||||
title->addWidget(button);
|
||||
}
|
||||
connect(m_fit, &QPushButton::clicked, this, [this] { m_preview->setZoom(std::nullopt); });
|
||||
connect(m_zoomIn, &QPushButton::clicked, this, [this] { zoomBy(1); });
|
||||
connect(m_zoomOut, &QPushButton::clicked, this, [this] { zoomBy(-1); });
|
||||
top->addLayout(title);
|
||||
m_preview->setObjectName(QStringLiteral("exportPreview"));
|
||||
m_spinner->setObjectName(QStringLiteral("exportSpinner"));
|
||||
m_spinner->setRange(0, 0);
|
||||
m_spinner->setTextVisible(false);
|
||||
m_spinner->setGeometry(264, 149, 32, 32);
|
||||
top->addWidget(m_preview);
|
||||
column->addLayout(top);
|
||||
|
||||
auto *grid = new QGridLayout;
|
||||
grid->setHorizontalSpacing(12);
|
||||
grid->setVerticalSpacing(12);
|
||||
// Swift's segmented picker: touching buttons, one checked.
|
||||
auto *formats = new QHBoxLayout;
|
||||
formats->setSpacing(0);
|
||||
for (const ExportFormat each : allExportFormats) {
|
||||
auto *button = new QToolButton(this);
|
||||
button->setText(rawValue(each));
|
||||
button->setObjectName(QStringLiteral("exportFormat") + rawValue(each));
|
||||
button->setCheckable(true);
|
||||
button->setChecked(each == format);
|
||||
m_formats->addButton(button, int(each));
|
||||
formats->addWidget(button);
|
||||
}
|
||||
formats->addStretch(1);
|
||||
connect(m_formats, &QButtonGroup::idClicked, this, [this](int id) {
|
||||
m_settings.format = ExportFormat(id);
|
||||
request();
|
||||
});
|
||||
grid->addWidget(new QLabel(QStringLiteral("Format"), this), 0, 0);
|
||||
grid->addLayout(formats, 0, 1);
|
||||
auto *size = new QHBoxLayout;
|
||||
size->setSpacing(6);
|
||||
m_width->setObjectName(QStringLiteral("exportWidth"));
|
||||
m_width->setToolTip(QStringLiteral("Width in pixels; the height follows, keeping the proportions"));
|
||||
m_height->setObjectName(QStringLiteral("exportHeight"));
|
||||
m_height->setToolTip(QStringLiteral("Height in pixels; the width follows, keeping the proportions"));
|
||||
for (PickerField *field : {m_width, m_height}) {
|
||||
field->setFixedWidth(64);
|
||||
field->setAlignment(Qt::AlignRight);
|
||||
}
|
||||
size->addWidget(m_width);
|
||||
size->addWidget(text(QStringLiteral("×"), 13, QFont::Normal, QPalette::PlaceholderText, this));
|
||||
size->addWidget(m_height);
|
||||
size->addWidget(text(QStringLiteral("px"), 13, QFont::Normal, QPalette::PlaceholderText, this));
|
||||
m_scale->setObjectName(QStringLiteral("exportScale"));
|
||||
m_scale->setToolTip(QStringLiteral("Scale the export: the document stays as it is"));
|
||||
m_scale->setPopupMode(QToolButton::InstantPopup);
|
||||
auto *scales = new QMenu(m_scale);
|
||||
for (const int percent : {25, 50, 75, 100, 200})
|
||||
connect(scales->addAction(QStringLiteral("%1%").arg(percent)), &QAction::triggered, this, [this, percent] {
|
||||
setScale(percent / 100.0);
|
||||
// The sheet's own change replaces any typing.
|
||||
showSize();
|
||||
synchronize();
|
||||
});
|
||||
m_scale->setMenu(scales);
|
||||
size->addWidget(m_scale);
|
||||
size->addStretch(1);
|
||||
grid->addWidget(new QLabel(QStringLiteral("Size"), this), 1, 0);
|
||||
grid->addLayout(size, 1, 1);
|
||||
// Swift's slider steps by hundredths.
|
||||
auto *qualityLabel = new QLabel(QStringLiteral("Quality"), this);
|
||||
qualityLabel->setBuddy(m_quality);
|
||||
m_quality->setObjectName(QStringLiteral("exportQuality"));
|
||||
m_quality->setRange(0, 100);
|
||||
m_quality->setFixedWidth(300);
|
||||
m_quality->setValue(int(std::lround(m_settings.options.quality * 100)));
|
||||
connect(m_quality, &QSlider::valueChanged, this, [this](int hundredths) {
|
||||
m_settings.options.quality = hundredths / 100.0;
|
||||
request();
|
||||
});
|
||||
m_percent->setObjectName(QStringLiteral("exportPercent"));
|
||||
m_percent->setFixedWidth(45);
|
||||
m_percent->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
auto *quality = new QWidget(this);
|
||||
auto *qualityRow = new QHBoxLayout(quality);
|
||||
qualityRow->setContentsMargins(0, 0, 0, 0);
|
||||
qualityRow->addWidget(m_quality);
|
||||
qualityRow->addWidget(m_percent);
|
||||
qualityRow->addStretch(1);
|
||||
grid->addWidget(qualityLabel, 2, 0);
|
||||
grid->addWidget(quality, 2, 1);
|
||||
m_jpegRow = {qualityLabel, quality};
|
||||
auto *pageLabel = new QLabel(QStringLiteral("Page"), this);
|
||||
m_page->setObjectName(QStringLiteral("exportPage"));
|
||||
grid->addWidget(pageLabel, 3, 0);
|
||||
grid->addWidget(m_page, 3, 1);
|
||||
m_pdfRow = {pageLabel, m_page};
|
||||
// JPEG and a page both need a colour beneath.
|
||||
auto *backgroundLabel = new QLabel(QStringLiteral("Background"), this);
|
||||
m_matteSwatch->setObjectName(QStringLiteral("exportBackground"));
|
||||
m_matteSwatch->setToolTip(QStringLiteral("Color that fills transparent areas"));
|
||||
grid->addWidget(backgroundLabel, 4, 0);
|
||||
grid->addWidget(m_matteSwatch, 4, 1, Qt::AlignLeft);
|
||||
m_backgroundRow = {backgroundLabel, m_matteSwatch};
|
||||
column->addLayout(grid);
|
||||
|
||||
auto *buttons = new QHBoxLayout;
|
||||
buttons->setSpacing(12);
|
||||
m_note->setObjectName(QStringLiteral("exportNote"));
|
||||
buttons->addWidget(m_note);
|
||||
buttons->addStretch(1);
|
||||
m_failure->setObjectName(QStringLiteral("exportError"));
|
||||
m_bytes->setObjectName(QStringLiteral("exportBytes"));
|
||||
m_updating->setObjectName(QStringLiteral("exportUpdating"));
|
||||
buttons->addWidget(m_failure);
|
||||
buttons->addWidget(m_bytes);
|
||||
buttons->addWidget(m_updating);
|
||||
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
|
||||
cancel->setObjectName(QStringLiteral("exportCancel"));
|
||||
cancel->setAutoDefault(false);
|
||||
connect(cancel, &QPushButton::clicked, this, [this] {
|
||||
DialogColorSwatch::closePicker(m_session);
|
||||
m_finish(std::nullopt);
|
||||
});
|
||||
m_export->setObjectName(QStringLiteral("exportExport"));
|
||||
m_export->setDefault(true);
|
||||
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
|
||||
NativeShortcut::bind(*this, m_export, cancel);
|
||||
// Export rests until a result is ready.
|
||||
connect(m_export, &QPushButton::clicked, this, [this] {
|
||||
DialogColorSwatch::closePicker(m_session);
|
||||
if (m_settings.format == ExportFormat::jpeg)
|
||||
QSettings().setValue(qualityKey, m_settings.options.quality);
|
||||
m_finish(Chosen{m_result.value().data, m_result.value().settings.format});
|
||||
});
|
||||
buttons->addWidget(cancel);
|
||||
buttons->addWidget(m_export);
|
||||
column->addLayout(buttons);
|
||||
|
||||
m_wait->setObjectName(QStringLiteral("exportWait"));
|
||||
m_wait->setSingleShot(true);
|
||||
m_wait->setInterval(200);
|
||||
m_wait->setTimerType(Qt::PreciseTimer);
|
||||
connect(m_wait, &QTimer::timeout, this, &ExportAsSheet::encode);
|
||||
bindZoomKeys();
|
||||
connect(&ShortcutSettings::shared(), &ShortcutSettings::changed, this, &ExportAsSheet::bindZoomKeys);
|
||||
request();
|
||||
}
|
||||
|
||||
// A window-modal sheet blocks the menus: it takes their keys.
|
||||
void ExportAsSheet::bindZoomKeys()
|
||||
{
|
||||
const std::array<QKeySequence, 4> originals{QKeySequence(Qt::CTRL | Qt::Key_0), QKeySequence(Qt::CTRL | Qt::Key_1),
|
||||
QKeySequence(Qt::CTRL | Qt::Key_Equal), QKeySequence(Qt::CTRL | Qt::Key_Minus)};
|
||||
const std::array<std::function<void()>, 4> actions{[this] { m_preview->setZoom(std::nullopt); }, [this] { m_preview->setZoom(1.0); },
|
||||
[this] { zoomBy(1); }, [this] { zoomBy(-1); }};
|
||||
for (size_t index = 0; index < m_zoomKeys.size(); ++index) {
|
||||
if (!m_zoomKeys[index]) {
|
||||
m_zoomKeys[index] = new QShortcut(this);
|
||||
connect(m_zoomKeys[index], &QShortcut::activated, this, actions[index]);
|
||||
}
|
||||
m_zoomKeys[index]->setKey(ShortcutSettings::shared().menu(originals[index]));
|
||||
}
|
||||
synchronize();
|
||||
}
|
||||
|
||||
// SwiftUI cancels the task when the sheet goes.
|
||||
ExportAsSheet::~ExportAsSheet()
|
||||
{
|
||||
m_cancelled->store(true);
|
||||
releaseFocus(*this);
|
||||
DialogColorSwatch::closePicker(m_session);
|
||||
}
|
||||
|
||||
// Swift's task(id:): the last request stops; this waits 200 ms.
|
||||
void ExportAsSheet::request()
|
||||
{
|
||||
if (m_cancelled)
|
||||
m_cancelled->store(true);
|
||||
m_cancelled = std::make_shared<std::atomic_bool>(false);
|
||||
m_error.reset();
|
||||
m_wait->start();
|
||||
synchronize();
|
||||
}
|
||||
|
||||
void ExportAsSheet::encode()
|
||||
{
|
||||
const std::shared_ptr<std::atomic_bool> cancelled = m_cancelled;
|
||||
const Settings requested = m_settings;
|
||||
auto *watcher = new QFutureWatcher<Made>(this);
|
||||
connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher, cancelled, requested] {
|
||||
watcher->deleteLater();
|
||||
// Swift's check after the await: a newer request supersedes.
|
||||
if (cancelled->load())
|
||||
return;
|
||||
Made made = watcher->future().takeResult();
|
||||
if (made.data)
|
||||
m_result = Encoded{requested, std::move(*made.data), std::move(made.preview)};
|
||||
else
|
||||
m_error = made.failure.value();
|
||||
synchronize();
|
||||
});
|
||||
watcher->setFuture(QtConcurrent::run([raster = m_raster, requested, cancelled]() -> Made {
|
||||
try {
|
||||
const ExportRaster sized = ImageExporter::resized(raster, requested.width, requested.height);
|
||||
const JPEGOptions &options = requested.options;
|
||||
const QColor background = QColor::fromRgbF(float(options.red), float(options.green), float(options.blue));
|
||||
switch (requested.format) {
|
||||
case ExportFormat::jpeg: {
|
||||
JPEGResult encoded = ImageExporter::jpeg(sized, options, [cancelled] { return cancelled->load(); });
|
||||
return {std::move(encoded.data), std::move(encoded.preview), std::nullopt};
|
||||
}
|
||||
case ExportFormat::png:
|
||||
return {ImageExporter::pngData(sized), sized.image, std::nullopt};
|
||||
case ExportFormat::pdf:
|
||||
return {ImageExporter::pdfData(sized, background), ImageExporter::flattened(sized, background), std::nullopt};
|
||||
}
|
||||
throw std::logic_error("unknown export format");
|
||||
} catch (const CancellationError &) {
|
||||
return {};
|
||||
} catch (const ExportError &error) {
|
||||
return {std::nullopt, QImage(), QString::fromUtf8(error.what())};
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void ExportAsSheet::setMatte(const PaletteColor &matte)
|
||||
{
|
||||
JPEGOptions next = m_settings.options;
|
||||
next.red = matte.red;
|
||||
next.green = matte.green;
|
||||
next.blue = matte.blue;
|
||||
// Equal settings ask nothing, as Swift's task id.
|
||||
if (next == m_settings.options)
|
||||
return;
|
||||
m_settings.options = next;
|
||||
request();
|
||||
}
|
||||
|
||||
void ExportAsSheet::setScale(double scale)
|
||||
{
|
||||
const double width = m_raster.image.width(), height = m_raster.image.height();
|
||||
const double largest = std::min(DocumentLimits::maxSide / std::max(width, height), std::sqrt(double(DocumentLimits::maxSurfacePixels) / (width * height)));
|
||||
const double clamped = std::min(largest, std::max(0.01, std::isfinite(scale) ? scale : 1.0));
|
||||
const Settings before = m_settings;
|
||||
m_settings.width = std::max(1, int(std::lround(width * clamped)));
|
||||
m_settings.height = std::max(1, int(std::lround(height * clamped)));
|
||||
if (m_settings != before)
|
||||
request();
|
||||
}
|
||||
|
||||
void ExportAsSheet::showSize()
|
||||
{
|
||||
m_width->setText(QString::number(m_settings.width));
|
||||
m_height->setText(QString::number(m_settings.height));
|
||||
}
|
||||
|
||||
double ExportAsSheet::shownZoom() const
|
||||
{
|
||||
return m_preview->zoom().value_or(JPEGPreview::fitZoom(m_settings.width, m_settings.height, QSizeF(JPEGPreview::frame), devicePixelRatioF()));
|
||||
}
|
||||
|
||||
void ExportAsSheet::zoomBy(int direction)
|
||||
{
|
||||
if (const std::optional<double> next = JPEGPreview::step(shownZoom(), direction))
|
||||
m_preview->setZoom(next);
|
||||
}
|
||||
|
||||
void ExportAsSheet::changeEvent(QEvent *event)
|
||||
{
|
||||
QWidget::changeEvent(event);
|
||||
if (event->type() == QEvent::PaletteChange)
|
||||
synchronize();
|
||||
}
|
||||
|
||||
void ExportAsSheet::synchronize()
|
||||
{
|
||||
const bool ready = m_result && m_result->settings == m_settings && !m_error;
|
||||
const bool updating = !ready && !m_error;
|
||||
m_preview->setImage(m_result ? m_result->preview : QImage(), m_result ? QSize(m_result->settings.width, m_result->settings.height) : QSize(), updating);
|
||||
m_spinner->setVisible(updating);
|
||||
// A field keeps its typing while it holds the keys.
|
||||
if (!m_width->hasFocus())
|
||||
m_width->setText(QString::number(m_settings.width));
|
||||
if (!m_height->hasFocus())
|
||||
m_height->setText(QString::number(m_settings.height));
|
||||
m_scale->setText(QStringLiteral("%1%").arg(std::lround(double(m_settings.width) / m_raster.image.width() * 100)));
|
||||
const ExportFormat format = m_settings.format;
|
||||
for (QWidget *each : m_jpegRow)
|
||||
each->setVisible(format == ExportFormat::jpeg);
|
||||
for (QWidget *each : m_pdfRow)
|
||||
each->setVisible(format == ExportFormat::pdf);
|
||||
for (QWidget *each : m_backgroundRow)
|
||||
each->setVisible(format != ExportFormat::png);
|
||||
m_percent->setText(QStringLiteral("%1%").arg(std::lround(m_settings.options.quality * 100)));
|
||||
// The page: the export's pixels at the document's resolution.
|
||||
const double dpi = m_raster.resolution > 0 ? m_raster.resolution : 72;
|
||||
m_page->setText(QStringLiteral("%1 × %2 in at %3 DPI").arg(inches(m_settings.width / dpi), inches(m_settings.height / dpi)).arg(std::lround(dpi)));
|
||||
m_note->setText(format == ExportFormat::png ? QStringLiteral("Transparency kept · sRGB") : QStringLiteral("sRGB"));
|
||||
m_failure->setVisible(m_error.has_value());
|
||||
m_failure->setText(m_error.value_or(QString()));
|
||||
m_bytes->setVisible(ready);
|
||||
m_bytes->setText(ready ? ByteCounts::file(m_result->data.size()) : QString());
|
||||
m_updating->setVisible(updating);
|
||||
m_export->setEnabled(ready);
|
||||
// The zoom buttons name their keys and the zoom.
|
||||
const double zoom = shownZoom();
|
||||
const QString percent = QStringLiteral("%1%").arg(std::lround(zoom * 100));
|
||||
const auto key = [this](size_t index) { return m_zoomKeys[index] ? m_zoomKeys[index]->key().toString(QKeySequence::NativeText) : QString(); };
|
||||
m_fit->setEnabled(m_preview->zoom().has_value());
|
||||
m_fit->setToolTip(QStringLiteral("Show the whole image (%1)").arg(key(0)));
|
||||
m_zoomIn->setEnabled(JPEGPreview::step(zoom, 1).has_value());
|
||||
m_zoomIn->setToolTip(QStringLiteral("Zoom in (%1), now %2. At 100% each pixel of the export is one pixel of the screen").arg(key(2), percent));
|
||||
m_zoomOut->setEnabled(JPEGPreview::step(zoom, -1).has_value());
|
||||
m_zoomOut->setToolTip(QStringLiteral("Zoom out (%1), now %2").arg(key(3), percent));
|
||||
const QColor ink = palette().color(QPalette::ButtonText);
|
||||
m_zoomIn->setIcon(LayerIcons::pixmap(LayerIcon::plusMagnifyingGlass, 16, ink, devicePixelRatioF()));
|
||||
m_zoomOut->setIcon(LayerIcons::pixmap(LayerIcon::minusMagnifyingGlass, 16, ink, devicePixelRatioF()));
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "Document/ColorPalette.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "IO/ProjectController.h"
|
||||
#include <QAbstractScrollArea>
|
||||
#include <QWidget>
|
||||
#include <array>
|
||||
@@ -8,9 +9,13 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
class QButtonGroup;
|
||||
class QLabel;
|
||||
class QProgressBar;
|
||||
class QToolButton;
|
||||
class PickerField;
|
||||
class QShortcut;
|
||||
class QPushButton;
|
||||
class QSlider;
|
||||
@@ -18,7 +23,7 @@ class QTimer;
|
||||
class DialogColorSwatch;
|
||||
class EditorSession;
|
||||
|
||||
// Swift's JPEGPreview: the encoded JPEG, fitted or zoomed, dragged.
|
||||
// Swift's JPEGPreview: the export, fitted or zoomed, dragged.
|
||||
class JPEGPreview : public QAbstractScrollArea {
|
||||
Q_OBJECT
|
||||
public:
|
||||
@@ -29,9 +34,9 @@ public:
|
||||
// The next step past `zoom`: 1 in, −1 out.
|
||||
static std::optional<double> step(double zoom, int direction);
|
||||
|
||||
JPEGPreview(std::function<void()> zoomChanged, QWidget *parent);
|
||||
// `pixels`: the export's size, which the image may undercut.
|
||||
JPEGPreview(QSize pixels, std::function<void()> zoomChanged, QWidget *parent);
|
||||
void setImage(const QImage &image, bool updating);
|
||||
void setImage(const QImage &image, QSize pixels, bool updating);
|
||||
// None fits; 1 is a screen pixel a pixel.
|
||||
std::optional<double> zoom() const { return m_zoom; }
|
||||
void setZoom(std::optional<double> zoom);
|
||||
@@ -47,11 +52,12 @@ protected:
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
static QPixmap checkerboard(double ratio);
|
||||
QSizeF shownSize(double zoom) const;
|
||||
void updateRanges();
|
||||
void showCursor(bool grabbing);
|
||||
|
||||
const QSize m_pixels;
|
||||
QSize m_pixels;
|
||||
// Each change of zoom, for the sheet's buttons.
|
||||
const std::function<void()> m_zoomChanged;
|
||||
QImage m_image;
|
||||
@@ -63,46 +69,80 @@ private:
|
||||
std::optional<std::pair<QPoint, QPoint>> m_drag;
|
||||
};
|
||||
|
||||
// Swift's JPEGExportSheet: quality, matte and the encoded preview.
|
||||
class JPEGExportSheet : public QWidget {
|
||||
// Swift's ExportAsSheet: PNG, JPEG or PDF, sized and previewed.
|
||||
class ExportAsSheet : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
// The last export's quality, where the next one starts.
|
||||
// The last export's JPEG quality, where the next one starts.
|
||||
static const QString qualityKey;
|
||||
// What Export… hands over: the bytes and their format.
|
||||
struct Chosen {
|
||||
QByteArray data;
|
||||
ExportFormat format;
|
||||
};
|
||||
// What is written; each change makes a new preview.
|
||||
struct Settings {
|
||||
ExportFormat format;
|
||||
JPEGOptions options;
|
||||
int width;
|
||||
int height;
|
||||
bool operator==(const Settings &) const = default;
|
||||
};
|
||||
|
||||
JPEGExportSheet(ExportRaster raster, EditorSession &session, std::function<void(std::optional<QByteArray>)> finish, QWidget *parent = nullptr);
|
||||
~JPEGExportSheet() override;
|
||||
ExportAsSheet(ExportRaster raster, EditorSession &session, ExportFormat format, std::function<void(std::optional<Chosen>)> finish,
|
||||
QWidget *parent = nullptr);
|
||||
~ExportAsSheet() override;
|
||||
|
||||
const Settings &settings() const { return m_settings; }
|
||||
|
||||
protected:
|
||||
// The zoom glyphs follow the theme.
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
||||
private:
|
||||
struct Encoded {
|
||||
Settings settings;
|
||||
QByteArray data;
|
||||
QImage preview;
|
||||
};
|
||||
void request();
|
||||
void encode();
|
||||
void setMatte(const PaletteColor &matte);
|
||||
// Both sides follow one, the proportions kept, within the limits.
|
||||
void setScale(double scale);
|
||||
// Both fields show the size, typing or not.
|
||||
void showSize();
|
||||
double shownZoom() const;
|
||||
void zoomBy(int direction);
|
||||
void synchronize();
|
||||
// The View menu's zoom keys, as remapped, zoom the preview.
|
||||
void bindZoomKeys();
|
||||
|
||||
const ExportRaster m_raster;
|
||||
EditorSession &m_session;
|
||||
const std::function<void(std::optional<QByteArray>)> m_finish;
|
||||
JPEGOptions m_options;
|
||||
std::optional<JPEGResult> m_result;
|
||||
std::optional<JPEGOptions> m_readyOptions;
|
||||
const std::function<void(std::optional<Chosen>)> m_finish;
|
||||
Settings m_settings;
|
||||
std::optional<Encoded> m_result;
|
||||
std::optional<QString> m_error;
|
||||
// The newest request's flag, Swift's task cancellation.
|
||||
std::shared_ptr<std::atomic_bool> m_cancelled;
|
||||
QTimer *const m_wait;
|
||||
JPEGPreview *const m_preview;
|
||||
QProgressBar *const m_spinner;
|
||||
QButtonGroup *const m_formats;
|
||||
PickerField *const m_width;
|
||||
PickerField *const m_height;
|
||||
QToolButton *const m_scale;
|
||||
QSlider *const m_quality;
|
||||
QLabel *const m_percent;
|
||||
QLabel *const m_page;
|
||||
DialogColorSwatch *const m_matteSwatch;
|
||||
// Rows shown by format: Quality for JPEG, Page for PDF.
|
||||
std::vector<QWidget *> m_jpegRow, m_pdfRow, m_backgroundRow;
|
||||
QLabel *const m_note;
|
||||
QLabel *const m_failure;
|
||||
QLabel *const m_bytes;
|
||||
QLabel *const m_note;
|
||||
QLabel *const m_updating;
|
||||
QPushButton *const m_export;
|
||||
QPushButton *const m_fit;
|
||||
QPushButton *const m_zoomIn;
|
||||
@@ -1,282 +0,0 @@
|
||||
#include "UI/JPEGExportSheet.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "UI/ByteCounts.h"
|
||||
#include "UI/ColorPickerSheet+Dialog.h"
|
||||
#include "UI/LayerIcons.h"
|
||||
#include <QFutureWatcher>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QShortcut>
|
||||
#include <QSlider>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtConcurrent>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
const QString JPEGExportSheet::qualityKey = QStringLiteral("jpegExportQuality");
|
||||
|
||||
namespace {
|
||||
QLabel *text(const QString &words, int pixels, QFont::Weight weight, QPalette::ColorRole role, QWidget *parent)
|
||||
{
|
||||
auto *label = new QLabel(words, parent);
|
||||
label->setTextFormat(Qt::PlainText);
|
||||
QFont font = label->font();
|
||||
font.setPixelSize(pixels);
|
||||
font.setWeight(weight);
|
||||
label->setFont(font);
|
||||
label->setForegroundRole(role);
|
||||
return label;
|
||||
}
|
||||
|
||||
QString grouped(qint64 value)
|
||||
{
|
||||
return QLocale(QLocale::English, QLocale::UnitedStates).toString(value);
|
||||
}
|
||||
|
||||
// Swift reads a finite number alone, clamped to 0–1.
|
||||
double startQuality()
|
||||
{
|
||||
bool number = false;
|
||||
const double saved = QSettings().value(JPEGExportSheet::qualityKey).toDouble(&number);
|
||||
return number && std::isfinite(saved) ? std::clamp(saved, 0.0, 1.0) : JPEGOptions().quality;
|
||||
}
|
||||
|
||||
struct Encoded {
|
||||
std::optional<JPEGResult> result;
|
||||
std::optional<QString> failure;
|
||||
};
|
||||
}
|
||||
|
||||
JPEGExportSheet::JPEGExportSheet(ExportRaster raster, EditorSession &session, std::function<void(std::optional<QByteArray>)> finish, QWidget *parent)
|
||||
: QWidget(parent), m_raster(std::move(raster)), m_session(session), m_finish(std::move(finish)), m_wait(new QTimer(this)), m_preview(new JPEGPreview(m_raster.image.size(), [this] { synchronize(); }, this)),
|
||||
m_spinner(new QProgressBar(m_preview)), m_quality(new QSlider(Qt::Horizontal, this)),
|
||||
m_percent(text(QString(), 13, QFont::Normal, QPalette::WindowText, this)), m_matteSwatch(new DialogColorSwatch(
|
||||
QStringLiteral("JPEG Background"), [this] { return PaletteColor{m_options.red, m_options.green, m_options.blue}; },
|
||||
[this](const PaletteColor &matte) { setMatte(matte); }, session, this)),
|
||||
m_failure(text(QString(), 13, QFont::Normal, QPalette::BrightText, this)), m_bytes(text(QString(), 13, QFont::Normal, QPalette::WindowText, this)),
|
||||
m_note(text(QString(), 13, QFont::Normal, QPalette::PlaceholderText, this)), m_export(new QPushButton(QStringLiteral("Export…"), this)),
|
||||
m_fit(new QPushButton(QStringLiteral("Fit"), this)), m_zoomIn(new QPushButton(this)), m_zoomOut(new QPushButton(this))
|
||||
{
|
||||
m_options.quality = startQuality();
|
||||
auto *column = new QVBoxLayout(this);
|
||||
column->setContentsMargins(24, 24, 24, 24);
|
||||
column->setSpacing(16);
|
||||
// The title row sits 8 above; it inherits 8.
|
||||
auto *top = new QVBoxLayout;
|
||||
top->setSpacing(8);
|
||||
auto *title = new QHBoxLayout;
|
||||
title->addWidget(text(QStringLiteral("Export JPEG"), 17, QFont::Bold, QPalette::WindowText, this));
|
||||
title->addStretch(1);
|
||||
m_fit->setObjectName(QStringLiteral("jpegFit"));
|
||||
m_zoomIn->setObjectName(QStringLiteral("jpegZoomIn"));
|
||||
m_zoomIn->setAccessibleName(QStringLiteral("Zoom in"));
|
||||
m_zoomOut->setObjectName(QStringLiteral("jpegZoomOut"));
|
||||
m_zoomOut->setAccessibleName(QStringLiteral("Zoom out"));
|
||||
for (QPushButton *button : {m_fit, m_zoomIn, m_zoomOut}) {
|
||||
button->setAutoDefault(false);
|
||||
title->addWidget(button);
|
||||
}
|
||||
connect(m_fit, &QPushButton::clicked, this, [this] { m_preview->setZoom(std::nullopt); });
|
||||
connect(m_zoomIn, &QPushButton::clicked, this, [this] { m_preview->zoomBy(1); });
|
||||
connect(m_zoomOut, &QPushButton::clicked, this, [this] { m_preview->zoomBy(-1); });
|
||||
top->addLayout(title);
|
||||
m_preview->setObjectName(QStringLiteral("jpegPreview"));
|
||||
m_spinner->setObjectName(QStringLiteral("jpegSpinner"));
|
||||
m_spinner->setRange(0, 0);
|
||||
m_spinner->setTextVisible(false);
|
||||
m_spinner->setGeometry(264, 149, 32, 32);
|
||||
top->addWidget(m_preview);
|
||||
column->addLayout(top);
|
||||
|
||||
auto *quality = new QHBoxLayout;
|
||||
auto *qualityLabel = new QLabel(QStringLiteral("Quality"), this);
|
||||
qualityLabel->setBuddy(m_quality);
|
||||
// Swift's slider steps by hundredths.
|
||||
m_quality->setObjectName(QStringLiteral("jpegQuality"));
|
||||
m_quality->setRange(0, 100);
|
||||
m_quality->setValue(int(std::lround(m_options.quality * 100)));
|
||||
connect(m_quality, &QSlider::valueChanged, this, [this](int hundredths) {
|
||||
m_options.quality = hundredths / 100.0;
|
||||
request();
|
||||
});
|
||||
m_percent->setObjectName(QStringLiteral("jpegPercent"));
|
||||
m_percent->setFixedWidth(45);
|
||||
m_percent->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
quality->addWidget(qualityLabel);
|
||||
quality->addWidget(m_quality, 1);
|
||||
quality->addWidget(m_percent);
|
||||
column->addLayout(quality);
|
||||
|
||||
// Its label and the app's picker on a swatch.
|
||||
auto *matte = new QHBoxLayout;
|
||||
matte->setSpacing(8);
|
||||
matte->addWidget(new QLabel(QStringLiteral("Background for transparency"), this));
|
||||
m_matteSwatch->setObjectName(QStringLiteral("jpegMatte"));
|
||||
m_matteSwatch->setToolTip(QStringLiteral("Color that fills transparent areas"));
|
||||
matte->addWidget(m_matteSwatch);
|
||||
matte->addStretch(1);
|
||||
column->addLayout(matte);
|
||||
|
||||
// The size, the result and the buttons share one row.
|
||||
auto *buttons = new QHBoxLayout;
|
||||
buttons->setSpacing(12);
|
||||
QLabel *size = text(QStringLiteral("%1 × %2 px · sRGB").arg(grouped(m_raster.image.width()), grouped(m_raster.image.height())), 13, QFont::Normal,
|
||||
QPalette::PlaceholderText, this);
|
||||
size->setObjectName(QStringLiteral("jpegSize"));
|
||||
buttons->addWidget(size);
|
||||
buttons->addStretch(1);
|
||||
m_failure->setObjectName(QStringLiteral("jpegError"));
|
||||
m_bytes->setObjectName(QStringLiteral("jpegBytes"));
|
||||
m_note->setObjectName(QStringLiteral("jpegNote"));
|
||||
m_note->setText(QStringLiteral("Updating…"));
|
||||
buttons->addWidget(m_failure);
|
||||
buttons->addWidget(m_bytes);
|
||||
buttons->addWidget(m_note);
|
||||
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
|
||||
cancel->setObjectName(QStringLiteral("jpegCancel"));
|
||||
cancel->setAutoDefault(false);
|
||||
connect(cancel, &QPushButton::clicked, this, [this] {
|
||||
DialogColorSwatch::closePicker(m_session);
|
||||
m_finish(std::nullopt);
|
||||
});
|
||||
m_export->setObjectName(QStringLiteral("jpegExport"));
|
||||
m_export->setDefault(true);
|
||||
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
|
||||
NativeShortcut::bind(*this, m_export, cancel);
|
||||
// Export rests until a result is ready.
|
||||
connect(m_export, &QPushButton::clicked, this, [this] {
|
||||
DialogColorSwatch::closePicker(m_session);
|
||||
QSettings().setValue(qualityKey, m_options.quality);
|
||||
m_finish(m_result.value().data);
|
||||
});
|
||||
buttons->addWidget(cancel);
|
||||
buttons->addWidget(m_export);
|
||||
column->addLayout(buttons);
|
||||
|
||||
m_wait->setObjectName(QStringLiteral("jpegWait"));
|
||||
m_wait->setSingleShot(true);
|
||||
m_wait->setInterval(200);
|
||||
m_wait->setTimerType(Qt::PreciseTimer);
|
||||
connect(m_wait, &QTimer::timeout, this, &JPEGExportSheet::encode);
|
||||
bindZoomKeys();
|
||||
connect(&ShortcutSettings::shared(), &ShortcutSettings::changed, this, &JPEGExportSheet::bindZoomKeys);
|
||||
request();
|
||||
}
|
||||
|
||||
// A window-modal sheet blocks the menus: it takes their keys.
|
||||
void JPEGExportSheet::bindZoomKeys()
|
||||
{
|
||||
const std::array<QKeySequence, 4> originals{QKeySequence(Qt::CTRL | Qt::Key_0), QKeySequence(Qt::CTRL | Qt::Key_1),
|
||||
QKeySequence(Qt::CTRL | Qt::Key_Equal), QKeySequence(Qt::CTRL | Qt::Key_Minus)};
|
||||
const std::array<std::function<void()>, 4> actions{[this] { m_preview->setZoom(std::nullopt); }, [this] { m_preview->setZoom(1.0); },
|
||||
[this] { m_preview->zoomBy(1); }, [this] { m_preview->zoomBy(-1); }};
|
||||
for (size_t index = 0; index < m_zoomKeys.size(); ++index) {
|
||||
if (!m_zoomKeys[index]) {
|
||||
m_zoomKeys[index] = new QShortcut(this);
|
||||
connect(m_zoomKeys[index], &QShortcut::activated, this, actions[index]);
|
||||
}
|
||||
m_zoomKeys[index]->setKey(ShortcutSettings::shared().menu(originals[index]));
|
||||
}
|
||||
synchronize();
|
||||
}
|
||||
|
||||
// SwiftUI cancels the task when the sheet goes.
|
||||
JPEGExportSheet::~JPEGExportSheet()
|
||||
{
|
||||
m_cancelled->store(true);
|
||||
DialogColorSwatch::closePicker(m_session);
|
||||
}
|
||||
|
||||
// Swift's task(id:): the last request stops; this waits 200 ms.
|
||||
void JPEGExportSheet::request()
|
||||
{
|
||||
if (m_cancelled)
|
||||
m_cancelled->store(true);
|
||||
m_cancelled = std::make_shared<std::atomic_bool>(false);
|
||||
m_error.reset();
|
||||
m_wait->start();
|
||||
synchronize();
|
||||
}
|
||||
|
||||
void JPEGExportSheet::encode()
|
||||
{
|
||||
const std::shared_ptr<std::atomic_bool> cancelled = m_cancelled;
|
||||
const JPEGOptions requested = m_options;
|
||||
auto *watcher = new QFutureWatcher<Encoded>(this);
|
||||
connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher, cancelled, requested] {
|
||||
watcher->deleteLater();
|
||||
// Swift's check after the await: a newer request supersedes.
|
||||
if (cancelled->load())
|
||||
return;
|
||||
Encoded encoded = watcher->future().takeResult();
|
||||
if (encoded.result) {
|
||||
m_result = std::move(encoded.result);
|
||||
m_readyOptions = requested;
|
||||
} else {
|
||||
m_error = encoded.failure.value();
|
||||
}
|
||||
synchronize();
|
||||
});
|
||||
watcher->setFuture(QtConcurrent::run([raster = m_raster, requested, cancelled]() -> Encoded {
|
||||
try {
|
||||
return {ImageExporter::jpeg(raster, requested, [cancelled] { return cancelled->load(); }), std::nullopt};
|
||||
} catch (const CancellationError &) {
|
||||
return {};
|
||||
} catch (const ExportError &error) {
|
||||
return {std::nullopt, QString::fromUtf8(error.what())};
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void JPEGExportSheet::setMatte(const PaletteColor &matte)
|
||||
{
|
||||
JPEGOptions next = m_options;
|
||||
next.red = matte.red;
|
||||
next.green = matte.green;
|
||||
next.blue = matte.blue;
|
||||
// Equal options ask nothing, as Swift's task id.
|
||||
if (next == m_options)
|
||||
return;
|
||||
m_options = next;
|
||||
request();
|
||||
}
|
||||
|
||||
void JPEGExportSheet::changeEvent(QEvent *event)
|
||||
{
|
||||
QWidget::changeEvent(event);
|
||||
if (event->type() == QEvent::PaletteChange)
|
||||
synchronize();
|
||||
}
|
||||
|
||||
void JPEGExportSheet::synchronize()
|
||||
{
|
||||
const bool ready = m_result && m_readyOptions == m_options;
|
||||
const bool updating = m_readyOptions != m_options && !m_error;
|
||||
m_preview->setImage(m_result ? m_result->preview : QImage(), updating);
|
||||
m_spinner->setVisible(updating);
|
||||
m_percent->setText(QStringLiteral("%1%").arg(std::lround(m_options.quality * 100)));
|
||||
m_failure->setVisible(m_error.has_value());
|
||||
m_failure->setText(m_error.value_or(QString()));
|
||||
m_bytes->setVisible(ready && !m_error);
|
||||
m_bytes->setText(m_result ? ByteCounts::file(m_result->data.size()) : QString());
|
||||
m_note->setVisible(!ready && !m_error);
|
||||
m_export->setEnabled(ready && !m_error);
|
||||
// The zoom buttons name their keys and the zoom.
|
||||
const double zoom = m_preview->shownZoom();
|
||||
const QString percent = QStringLiteral("%1%").arg(std::lround(zoom * 100));
|
||||
const auto key = [this](size_t index) { return m_zoomKeys[index] ? m_zoomKeys[index]->key().toString(QKeySequence::NativeText) : QString(); };
|
||||
m_fit->setEnabled(m_preview->zoom().has_value());
|
||||
m_fit->setToolTip(QStringLiteral("Show the whole image (%1)").arg(key(0)));
|
||||
m_zoomIn->setEnabled(JPEGPreview::step(zoom, 1).has_value());
|
||||
m_zoomIn->setToolTip(QStringLiteral("Zoom in (%1), now %2. At 100% each pixel of the JPEG is one pixel of the screen, as on the canvas").arg(key(2), percent));
|
||||
m_zoomOut->setEnabled(JPEGPreview::step(zoom, -1).has_value());
|
||||
m_zoomOut->setToolTip(QStringLiteral("Zoom out (%1), now %2").arg(key(3), percent));
|
||||
const QColor ink = palette().color(QPalette::ButtonText);
|
||||
m_zoomIn->setIcon(LayerIcons::pixmap(LayerIcon::plusMagnifyingGlass, 16, ink, devicePixelRatioF()));
|
||||
m_zoomOut->setIcon(LayerIcons::pixmap(LayerIcon::minusMagnifyingGlass, 16, ink, devicePixelRatioF()));
|
||||
}
|
||||
@@ -124,7 +124,7 @@ const std::vector<ShortcutDefinition> &ShortcutDefinition::all()
|
||||
std::vector<ShortcutDefinition> result{
|
||||
entry("Undo", "z", 1, true), entry("Redo", "z", 9, true), entry("New Canvas", "n", 1, true),
|
||||
entry("Open Project", "o", 1, true), entry("Save", "s", 1, true), entry("Save As", "s", 9, true),
|
||||
entry("Export PNG", "e", 9, true), entry("Export JPEG", "s", 11, true), entry("Close Project", "w", 1, true),
|
||||
entry("Export PNG", "e", 9, true), entry("Export JPEG", "s", 11, true), entry("Export As", "w", 11, true), entry("Close Project", "w", 1, true),
|
||||
entry("Fit Canvas", "0", 1, true), entry("Search Commands", "f", 1, true), entry("Actual Pixels", "1", 1, true), entry("Zoom In", "=", 1, true),
|
||||
entry("Zoom Out", "-", 1, true), entry("Show Transform Controls", "h", 1, true), entry("Cut", "x", 1, true),
|
||||
entry("Copy", "c", 1, true), entry("Copy Merged", "c", 9, true), entry("Paste", "v", 1, true),
|
||||
|
||||
Reference in new issue
Block a user