diff --git a/ChangeLog b/ChangeLog index eef80d5843..eb488a0783 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,6 @@ Unreleased Version 2.2.2-pre * Fix: Solve rendering glitches with selection outlines that happen on some systems. Thanks xxxx for reporting. + * Feature: Allow scaling animation exports. Thanks Hopfel for animating across a giant canvas. 2024-08-09 Version 2.2.2-beta.3 * Fix: Use more accurate timers for performance profiles if the platform supports it. diff --git a/src/desktop/dialogs/animationexportdialog.cpp b/src/desktop/dialogs/animationexportdialog.cpp index add0f92cf6..0df12feeff 100644 --- a/src/desktop/dialogs/animationexportdialog.cpp +++ b/src/desktop/dialogs/animationexportdialog.cpp @@ -6,6 +6,7 @@ #include "libclient/canvas/documentmetadata.h" #include "libclient/canvas/paintengine.h" #include "libclient/export/animationformat.h" +#include #include #include #include @@ -21,7 +22,8 @@ namespace dialogs { -AnimationExportDialog::AnimationExportDialog(QWidget *parent) +AnimationExportDialog::AnimationExportDialog( + int scalePercent, bool scaleSmooth, QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Export Animation")); @@ -62,6 +64,22 @@ AnimationExportDialog::AnimationExportDialog(QWidget *parent) m_loopsSpinner->setRange(1, 99); outputForm->addRow(m_loopsLabel, m_loopsSpinner); + m_scaleSpinner = new QSpinBox; + m_scaleSpinner->setRange(1, 1000); + m_scaleSpinner->setValue(scalePercent); + m_scaleSpinner->setSuffix(tr("%")); + + m_scaleSmoothBox = new QCheckBox(tr("Smooth")); + m_scaleSmoothBox->setChecked(scaleSmooth); + + QHBoxLayout *scaleLayout = new QHBoxLayout; + scaleLayout->addWidget(m_scaleSpinner); + scaleLayout->addWidget(m_scaleSmoothBox); + outputForm->addRow(tr("Scale:"), scaleLayout); + + m_scaleLabel = new QLabel; + outputForm->addRow(m_scaleLabel); + QWidget *inputWidget = new QWidget; QFormLayout *inputForm = new QFormLayout(inputWidget); tabs->addTab(inputWidget, tr("Input")); @@ -128,6 +146,9 @@ AnimationExportDialog::AnimationExportDialog(QWidget *parent) connect( m_formatCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &AnimationExportDialog::updateOutputUi); + connect( + m_scaleSpinner, QOverload::of(&QSpinBox::valueChanged), this, + &AnimationExportDialog::updateScalingUi); connect( m_startSpinner, QOverload::of(&QSpinBox::valueChanged), this, &AnimationExportDialog::updateEndRange); @@ -137,15 +158,27 @@ AnimationExportDialog::AnimationExportDialog(QWidget *parent) connect( m_x1Spinner, QOverload::of(&QSpinBox::valueChanged), this, &AnimationExportDialog::updateX2Range); + connect( + m_x1Spinner, QOverload::of(&QSpinBox::valueChanged), this, + &AnimationExportDialog::updateScalingUi); connect( m_x2Spinner, QOverload::of(&QSpinBox::valueChanged), this, &AnimationExportDialog::updateX1Range); + connect( + m_x2Spinner, QOverload::of(&QSpinBox::valueChanged), this, + &AnimationExportDialog::updateScalingUi); connect( m_y1Spinner, QOverload::of(&QSpinBox::valueChanged), this, &AnimationExportDialog::updateY2Range); + connect( + m_y1Spinner, QOverload::of(&QSpinBox::valueChanged), this, + &AnimationExportDialog::updateScalingUi); connect( m_y2Spinner, QOverload::of(&QSpinBox::valueChanged), this, &AnimationExportDialog::updateY1Range); + connect( + m_y2Spinner, QOverload::of(&QSpinBox::valueChanged), this, + &AnimationExportDialog::updateScalingUi); connect( m_inputResetButton, &QPushButton::clicked, this, &AnimationExportDialog::resetInputs); @@ -163,6 +196,7 @@ AnimationExportDialog::AnimationExportDialog(QWidget *parent) &AnimationExportDialog::requestExport, Qt::DirectConnection); updateOutputUi(); + updateScalingUi(); } void AnimationExportDialog::setCanvas(canvas::CanvasModel *canvas) @@ -201,6 +235,13 @@ void AnimationExportDialog::setFlipbookState( } } +QSize AnimationExportDialog::getScaledSizeFor( + int scalePercent, const QRect &rect) +{ + QSize size = rect.size() * (scalePercent / 100.0); + return QSize(qMax(size.width(), 1), qMax(size.height(), 1)); +} + #ifndef __EMSCRIPTEN__ void AnimationExportDialog::accept() { @@ -220,6 +261,16 @@ void AnimationExportDialog::updateOutputUi() m_loopsSpinner->setVisible(showLoops); } +void AnimationExportDialog::updateScalingUi() +{ + int scalePercent = m_scaleSpinner->value(); + m_scaleSmoothBox->setEnabled(scalePercent != 100); + QSize size = getScaledSizeFor(scalePercent, getCropRect()); + m_scaleLabel->setText(tr("Output resolution will be %1x%2 pixels.") + .arg(size.width()) + .arg(size.height())); +} + #ifndef __EMSCRIPTEN__ QString AnimationExportDialog::choosePath() { @@ -340,6 +391,7 @@ void AnimationExportDialog::setCanvasSize(const QSize &size) } m_canvasWidth = size.width(); m_canvasHeight = size.height(); + updateScalingUi(); } void AnimationExportDialog::setCanvasFrameCount(int frameCount) @@ -376,10 +428,15 @@ void AnimationExportDialog::requestExport() m_path, #endif format, m_loopsSpinner->value(), m_startSpinner->value() - 1, - m_endSpinner->value() - 1, m_framerateSpinner->value(), - QRect( - QPoint(m_x1Spinner->value(), m_y1Spinner->value()), - QPoint(m_x2Spinner->value(), m_y2Spinner->value()))); + m_endSpinner->value() - 1, m_framerateSpinner->value(), getCropRect(), + m_scaleSpinner->value(), m_scaleSmoothBox->isChecked()); +} + +QRect AnimationExportDialog::getCropRect() const +{ + return QRect( + QPoint(m_x1Spinner->value(), m_y1Spinner->value()), + QPoint(m_x2Spinner->value(), m_y2Spinner->value())); } } diff --git a/src/desktop/dialogs/animationexportdialog.h b/src/desktop/dialogs/animationexportdialog.h index 11d89548bd..5967cc37b3 100644 --- a/src/desktop/dialogs/animationexportdialog.h +++ b/src/desktop/dialogs/animationexportdialog.h @@ -4,6 +4,8 @@ #include #include +class KisSliderSpinBox; +class QCheckBox; class QComboBox; class QDialogButtonBox; class QLabel; @@ -20,7 +22,8 @@ namespace dialogs { class AnimationExportDialog final : public QDialog { Q_OBJECT public: - explicit AnimationExportDialog(QWidget *parent = nullptr); + explicit AnimationExportDialog( + int scalePercent, bool scaleSmooth, QWidget *parent = nullptr); void setCanvas(canvas::CanvasModel *canvas); @@ -28,6 +31,8 @@ class AnimationExportDialog final : public QDialog { int start, int end, double speedPercent, const QRectF &crop, bool apply); + static QSize getScaledSizeFor(int scalePercent, const QRect &rect); + public slots: #ifndef __EMSCRIPTEN__ void accept() override; @@ -39,10 +44,11 @@ public slots: const QString &path, #endif int format, int loops, int start, int end, int framerate, - const QRect &crop); + const QRect &crop, int scalePercent, bool scaleSmooth); private: void updateOutputUi(); + void updateScalingUi(); #ifndef __EMSCRIPTEN__ QString choosePath(); #endif @@ -62,11 +68,16 @@ public slots: void requestExport(); + QRect getCropRect() const; + #ifndef __EMSCRIPTEN__ QString m_path; #endif QComboBox *m_formatCombo; QLabel *m_loopsLabel; + QSpinBox *m_scaleSpinner; + QCheckBox *m_scaleSmoothBox; + QLabel *m_scaleLabel; QSpinBox *m_loopsSpinner; QSpinBox *m_startSpinner; QSpinBox *m_endSpinner; diff --git a/src/desktop/mainwindow.cpp b/src/desktop/mainwindow.cpp index b37e6ad2d4..b9e2cce18d 100644 --- a/src/desktop/mainwindow.cpp +++ b/src/desktop/mainwindow.cpp @@ -1927,7 +1927,8 @@ void MainWindow::showAnimationExportDialog(bool fromFlipbook) dlg->activateWindow(); dlg->raise(); } else { - dlg = new dialogs::AnimationExportDialog(this); + dlg = new dialogs::AnimationExportDialog( + m_animationExportScalePercent, m_animationExportScaleSmooth, this); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setObjectName(objectName); dlg->setCanvas(m_doc->canvas()); @@ -1958,19 +1959,33 @@ void MainWindow::exportAnimation( #ifndef __EMSCRIPTEN__ const QString &path, #endif - int format, int loops, int start, int end, int framerate, const QRect &crop) + int format, int loops, int start, int end, int framerate, const QRect &crop, + int scalePercent, bool scaleSmooth) { + m_animationExportScalePercent = scalePercent; + m_animationExportScaleSmooth = scaleSmooth; + QProgressDialog *progressDialog = new QProgressDialog( tr("Saving animation..."), tr("Cancel"), 0, 100, this); progressDialog->setMinimumDuration(500); progressDialog->setValue(0); + drawdance::CanvasState canvasState = + m_doc->canvas()->paintEngine()->viewCanvasState(); + QRect canvasRect = QRect(QPoint(0, 0), canvasState.size()); + QRect effectiveCrop = crop & canvasRect; + if(effectiveCrop.isEmpty()) { + effectiveCrop = canvasRect; + } + QSize size = dialogs::AnimationExportDialog::getScaledSizeFor( + scalePercent, effectiveCrop); + AnimationSaverRunnable *saver = new AnimationSaverRunnable( #ifndef __EMSCRIPTEN__ path, #endif - format, loops, start, end, framerate, crop, - m_doc->canvas()->paintEngine()->viewCanvasState(), this); + format, size.width(), size.height(), loops, start, end, framerate, + effectiveCrop, scaleSmooth, canvasState, this); saver->setAutoDelete(true); connect( diff --git a/src/desktop/mainwindow.h b/src/desktop/mainwindow.h index 239ea08ba8..b5b22d8a88 100644 --- a/src/desktop/mainwindow.h +++ b/src/desktop/mainwindow.h @@ -298,7 +298,7 @@ private slots: const QString &path, #endif int format, int loops, int start, int end, int framerate, - const QRect &crop); + const QRect &crop, int scalePercent, bool scaleSmooth); ActionBuilder makeAction(const char *name, const QString &text = QString{}); QAction *getAction(const QString &name); @@ -404,6 +404,8 @@ private slots: dialogs::SessionSettingsDialog *m_sessionSettings; dialogs::ServerLogDialog *m_serverLogDialog; dialogs::Flipbook::State m_flipbookState; + int m_animationExportScalePercent = 100; + bool m_animationExportScaleSmooth = true; #ifndef __EMSCRIPTEN__ QMenu *m_recentMenu; diff --git a/src/drawdance/libengine/dpengine/image.c b/src/drawdance/libengine/dpengine/image.c index b4373a2ee5..fff505a1ae 100644 --- a/src/drawdance/libengine/dpengine/image.c +++ b/src/drawdance/libengine/dpengine/image.c @@ -424,6 +424,34 @@ bool DP_image_thumbnail(DP_Image *img, DP_DrawContext *dc, int max_width, } } +DP_Image *DP_image_scale(DP_Image *img, DP_DrawContext *dc, int width, + int height, int interpolation) +{ + DP_ASSERT(img); + DP_ASSERT(dc); + if (width > 0 && height > 0) { + int src_width = DP_image_width(img); + int src_height = DP_image_height(img); + DP_Transform tf = DP_transform_scale( + DP_transform_identity(), + DP_int_to_double(width) / DP_int_to_double(src_width), + DP_int_to_double(height) / DP_int_to_double(src_height)); + DP_Image *result = DP_image_new(width, height); + if (DP_image_transform_draw(src_width, src_height, DP_image_pixels(img), + dc, result, tf, interpolation)) { + return result; + } + else { + DP_image_free(result); + return NULL; + } + } + else { + DP_error_set("Can't scale to zero dimensions"); + return NULL; + } +} + bool DP_image_same_pixel(DP_Image *img, DP_Pixel8 *out_pixel) { DP_Pixel8 *pixels = DP_image_pixels(img); diff --git a/src/drawdance/libengine/dpengine/image.h b/src/drawdance/libengine/dpengine/image.h index 9b73785656..d6eef35627 100644 --- a/src/drawdance/libengine/dpengine/image.h +++ b/src/drawdance/libengine/dpengine/image.h @@ -97,6 +97,9 @@ DP_Image *DP_image_transform(DP_Image *img, DP_DrawContext *dc, bool DP_image_thumbnail(DP_Image *img, DP_DrawContext *dc, int max_width, int max_height, DP_Image **out_thumb) DP_MUST_CHECK; +DP_Image *DP_image_scale(DP_Image *img, DP_DrawContext *dc, int width, + int height, int interpolation); + bool DP_image_same_pixel(DP_Image *img, DP_Pixel8 *out_pixel); DP_UPixelFloat DP_image_sample_color_at_with(int width, int height, diff --git a/src/drawdance/libimpex/dpimpex/save.c b/src/drawdance/libimpex/dpimpex/save.c index 201cb15232..d8b38eca15 100644 --- a/src/drawdance/libimpex/dpimpex/save.c +++ b/src/drawdance/libimpex/dpimpex/save.c @@ -880,8 +880,12 @@ static const char *get_path_separator(const char *path) struct DP_SaveFrameContext { DP_CanvasState *cs; + DP_DrawContext *dc; DP_ViewModeBuffer *vmbs; DP_Rect *crop; + int width; + int height; + int interpolation; int frame_count; const char *path; const char *separator; @@ -908,17 +912,53 @@ static void set_error_result(struct DP_SaveFrameContext *c, } } -static unsigned char *generate_frame_png(struct DP_SaveFrameContext *c, - DP_ViewModeBuffer *vmb, - int frame_index, size_t *out_size) +static DP_Image *generate_frame_image(DP_CanvasState *cs, DP_DrawContext *dc, + DP_Rect *crop, int width, int height, + int interpolation, DP_ViewModeBuffer *vmb, + int frame_index, DP_Mutex *mutex_or_null) { - DP_CanvasState *cs = c->cs; DP_ViewModeFilter vmf = DP_view_mode_filter_make_frame_render(vmb, cs, frame_index); DP_Image *img = DP_canvas_state_to_flat_image( - cs, DP_FLAT_IMAGE_RENDER_FLAGS, c->crop, &vmf); + cs, DP_FLAT_IMAGE_RENDER_FLAGS, crop, &vmf); if (!img) { DP_warn("Flatten frame %d: %s", frame_index, DP_error()); + return NULL; + } + + int input_width = DP_image_width(img); + int input_height = DP_image_height(img); + if (input_width == width && input_height == height) { + return img; + } + else { + if (mutex_or_null) { + DP_MUTEX_MUST_LOCK(mutex_or_null); + } + DP_Image *scaled_img = + DP_image_scale(img, dc, width, height, interpolation); + if (mutex_or_null) { + DP_MUTEX_MUST_UNLOCK(mutex_or_null); + } + DP_image_free(img); + if (scaled_img) { + return scaled_img; + } + else { + DP_warn("Scale frame %d: %s", frame_index, DP_error()); + return NULL; + } + } +} + +static unsigned char *generate_frame_png(struct DP_SaveFrameContext *c, + DP_ViewModeBuffer *vmb, + int frame_index, size_t *out_size) +{ + DP_Image *img = + generate_frame_image(c->cs, c->dc, c->crop, c->width, c->height, + c->interpolation, vmb, frame_index, c->mutex); + if (!img) { set_error_result(c, DP_SAVE_RESULT_FLATTEN_ERROR); return NULL; } @@ -975,13 +1015,35 @@ static void write_frame_to_zip(struct DP_SaveFrameContext *c, int frame_index, static char *save_frame(struct DP_SaveFrameContext *c, DP_ViewModeBuffer *vmb, int frame_index) { - DP_CanvasState *cs = c->cs; + DP_Image *img = + generate_frame_image(c->cs, c->dc, c->crop, c->width, c->height, + c->interpolation, vmb, frame_index, c->mutex); + if (!img) { + set_error_result(c, DP_SAVE_RESULT_FLATTEN_ERROR); + return NULL; + } + char *path = format_frame_path(c, frame_index); - DP_SaveResult result = save_flat_image( - cs, NULL, c->crop, path, save_png, - DP_view_mode_filter_make_frame_render(vmb, cs, frame_index), NULL, - NULL); - set_error_result(c, result); + DP_Output *output = DP_file_output_new_from_path(path); + if (!output) { + DP_free(path); + set_error_result(c, DP_SAVE_RESULT_OPEN_ERROR); + return NULL; + } + + if (!DP_image_write_png(img, output)) { + DP_output_free_discard(output); + DP_free(path); + set_error_result(c, DP_SAVE_RESULT_WRITE_ERROR); + return NULL; + } + + if (!DP_output_free(output)) { + DP_free(path); + set_error_result(c, DP_SAVE_RESULT_WRITE_ERROR); + return NULL; + } + return path; } @@ -1054,24 +1116,27 @@ static void save_frame_job(void *element, int thread_index) // Render and save the first frame given. char *path = save_frame(c, vmb, first_frame); report_progress(c); - // Subsequent frames are the same, so just copy the files. - int count = params->count; - for (int i = 1; i < count; ++i) { - if (DP_atomic_get(&c->result) == DP_SAVE_RESULT_SUCCESS) { - copy_frame(c, path, params->frames[i]); - report_progress(c); + if (path) { + // Subsequent frames are the same, so just copy the files. + int count = params->count; + for (int i = 1; i < count; ++i) { + if (DP_atomic_get(&c->result) == DP_SAVE_RESULT_SUCCESS) { + copy_frame(c, path, params->frames[i]); + report_progress(c); + } } + DP_free(path); } - DP_free(path); } } DP_free(params); } static DP_SaveResult -save_animation_frames(DP_CanvasState *cs, const char *path, +save_animation_frames(DP_CanvasState *cs, DP_DrawContext *dc, const char *path, DP_SaveAnimationProgressFn progress_fn, void *user, - DP_Rect *crop, int start, int end_inclusive, bool zip) + DP_Rect *crop, int width, int height, int interpolation, + int start, int end_inclusive, bool zip) { if (end_inclusive < start) { return DP_SAVE_RESULT_SUCCESS; @@ -1088,16 +1153,10 @@ save_animation_frames(DP_CanvasState *cs, const char *path, zw = NULL; } - DP_Mutex *mutex; - if (progress_fn) { - mutex = DP_mutex_new(); - if (!mutex) { - DP_zip_writer_free_abort(zw); - return DP_SAVE_RESULT_INTERNAL_ERROR; - } - } - else { - mutex = NULL; + DP_Mutex *mutex = DP_mutex_new(); + if (!mutex) { + DP_zip_writer_free_abort(zw); + return DP_SAVE_RESULT_INTERNAL_ERROR; } int frame_count = count_frames(start, end_inclusive); @@ -1113,8 +1172,12 @@ save_animation_frames(DP_CanvasState *cs, const char *path, int thread_count = DP_worker_thread_count(worker); struct DP_SaveFrameContext c = { cs, + dc, DP_malloc(sizeof(*c.vmbs) * DP_int_to_size(thread_count)), crop, + width, + height, + interpolation, frame_count, path, get_path_separator(path), @@ -1186,13 +1249,14 @@ save_animation_frames(DP_CanvasState *cs, const char *path, return result; } -DP_SaveResult DP_save_animation_frames(DP_CanvasState *cs, const char *path, - DP_Rect *crop, int start, - int end_inclusive, +DP_SaveResult DP_save_animation_frames(DP_CanvasState *cs, DP_DrawContext *dc, + const char *path, DP_Rect *crop, + int width, int height, int interpolation, + int start, int end_inclusive, DP_SaveAnimationProgressFn progress_fn, void *user) { - if (cs && path) { + if (cs && path && width > 0 && height > 0) { int frame_count = DP_canvas_state_frame_count(cs); if (start < 0) { start = 0; @@ -1204,7 +1268,8 @@ DP_SaveResult DP_save_animation_frames(DP_CanvasState *cs, const char *path, DP_PERF_BEGIN_DETAIL(fn, "animation_frames", "frame_count=%d,path=%s", count_frames(start, end_inclusive), path); DP_SaveResult result = save_animation_frames( - cs, path, progress_fn, user, crop, start, end_inclusive, false); + cs, dc, path, progress_fn, user, crop, width, height, interpolation, + start, end_inclusive, false); DP_PERF_END(fn); return result; } @@ -1213,12 +1278,14 @@ DP_SaveResult DP_save_animation_frames(DP_CanvasState *cs, const char *path, } } -DP_SaveResult DP_save_animation_zip(DP_CanvasState *cs, const char *path, - DP_Rect *crop, int start, int end_inclusive, +DP_SaveResult DP_save_animation_zip(DP_CanvasState *cs, DP_DrawContext *dc, + const char *path, DP_Rect *crop, int width, + int height, int interpolation, int start, + int end_inclusive, DP_SaveAnimationProgressFn progress_fn, void *user) { - if (cs && path) { + if (cs && path && width > 0 && height > 0) { int frame_count = DP_canvas_state_frame_count(cs); if (start < 0) { start = 0; @@ -1230,7 +1297,8 @@ DP_SaveResult DP_save_animation_zip(DP_CanvasState *cs, const char *path, DP_PERF_BEGIN_DETAIL(fn, "animation_zip", "frame_count=%d,path=%s", count_frames(start, end_inclusive), path); DP_SaveResult result = save_animation_frames( - cs, path, progress_fn, user, crop, start, end_inclusive, true); + cs, dc, path, progress_fn, user, crop, width, height, interpolation, + start, end_inclusive, true); DP_PERF_END(fn); return result; } @@ -1240,33 +1308,6 @@ DP_SaveResult DP_save_animation_zip(DP_CanvasState *cs, const char *path, } -static bool get_gif_dimensions(DP_CanvasState *cs, DP_Rect *crop, - int *out_width, int *out_height) -{ - DP_Rect rect = DP_rect_make(0, 0, DP_canvas_state_width(cs), - DP_canvas_state_height(cs)); - if (!DP_rect_valid(rect)) { - return false; - } - - if (crop) { - rect = DP_rect_intersection(rect, *crop); - if (!DP_rect_valid(rect)) { - return false; - } - } - - int width = DP_rect_width(rect); - int height = DP_rect_height(rect); - if (width < 1 || height < 1 || width > UINT16_MAX || height > UINT16_MAX) { - return false; - } - - *out_width = width; - *out_height = height; - return true; -} - static bool write_gif(void *user, const void *buffer, size_t size) { DP_Output *output = user; @@ -1298,16 +1339,12 @@ static bool report_gif_progress(DP_SaveAnimationProgressFn progress_fn, return !progress_fn || progress_fn(user, part / total); } -static DP_SaveResult save_animation_gif(DP_CanvasState *cs, const char *path, - DP_SaveAnimationProgressFn progress_fn, - void *user, DP_Rect *crop, int start, - int end_inclusive, int framerate) +static DP_SaveResult +save_animation_gif(DP_CanvasState *cs, DP_DrawContext *dc, const char *path, + DP_SaveAnimationProgressFn progress_fn, void *user, + DP_Rect *crop, int width, int height, int interpolation, + int start, int end_inclusive, int framerate) { - int width, height; - if (!get_gif_dimensions(cs, crop, &width, &height)) { - return DP_SAVE_RESULT_WRITE_ERROR; - } - DP_Output *output = DP_file_output_new_from_path(path); if (!output) { return DP_SAVE_RESULT_OPEN_ERROR; @@ -1337,10 +1374,8 @@ static DP_SaveResult save_animation_gif(DP_CanvasState *cs, const char *path, ++instances; } - DP_ViewModeFilter vmf = - DP_view_mode_filter_make_frame_render(&vmb, cs, i); - DP_Image *img = DP_canvas_state_to_flat_image( - cs, DP_FLAT_IMAGE_RENDER_FLAGS, crop, &vmf); + DP_Image *img = generate_frame_image(cs, dc, crop, width, height, + interpolation, &vmb, i, NULL); double delay = centiseconds_per_frame * DP_int_to_double(instances); double delay_floored = floor(delay + delay_frac); delay_frac = delay - delay_floored; @@ -1374,13 +1409,15 @@ static DP_SaveResult save_animation_gif(DP_CanvasState *cs, const char *path, return DP_SAVE_RESULT_SUCCESS; } -DP_SaveResult DP_save_animation_gif(DP_CanvasState *cs, const char *path, - DP_Rect *crop, int start, int end_inclusive, - int framerate, +DP_SaveResult DP_save_animation_gif(DP_CanvasState *cs, DP_DrawContext *dc, + const char *path, DP_Rect *crop, int width, + int height, int interpolation, int start, + int end_inclusive, int framerate, DP_SaveAnimationProgressFn progress_fn, void *user) { - if (cs && path) { + if (cs && path && width > 0 && height > 0 && width <= UINT16_MAX + && height <= UINT16_MAX) { int frame_count = DP_canvas_state_frame_count(cs); if (start < 0) { start = 0; @@ -1395,7 +1432,8 @@ DP_SaveResult DP_save_animation_gif(DP_CanvasState *cs, const char *path, DP_PERF_BEGIN_DETAIL(fn, "animation_gif", "frame_count=%d,path=%s", count_frames(start, end_inclusive), path); DP_SaveResult result = save_animation_gif( - cs, path, progress_fn, user, crop, start, end_inclusive, framerate); + cs, dc, path, progress_fn, user, crop, width, height, interpolation, + start, end_inclusive, framerate); DP_PERF_END(fn); return result; } diff --git a/src/drawdance/libimpex/dpimpex/save.h b/src/drawdance/libimpex/dpimpex/save.h index 6ccb01e1e1..e172f5e9b6 100644 --- a/src/drawdance/libimpex/dpimpex/save.h +++ b/src/drawdance/libimpex/dpimpex/save.h @@ -33,20 +33,24 @@ typedef bool (*DP_SaveAnimationProgressFn)(void *user, double progress); // To use the default values from the canvas state for the below functions, crop // can be NULL, start, end_inclusive and framerate can be -1. -DP_SaveResult DP_save_animation_frames(DP_CanvasState *cs, const char *path, - DP_Rect *crop, int start, - int end_inclusive, +DP_SaveResult DP_save_animation_frames(DP_CanvasState *cs, DP_DrawContext *dc, + const char *path, DP_Rect *crop, + int width, int height, int interpolation, + int start, int end_inclusive, DP_SaveAnimationProgressFn progress_fn, void *user); -DP_SaveResult DP_save_animation_zip(DP_CanvasState *cs, const char *path, - DP_Rect *crop, int start, int end_inclusive, +DP_SaveResult DP_save_animation_zip(DP_CanvasState *cs, DP_DrawContext *dc, + const char *path, DP_Rect *crop, int width, + int height, int interpolation, int start, + int end_inclusive, DP_SaveAnimationProgressFn progress_fn, void *user); -DP_SaveResult DP_save_animation_gif(DP_CanvasState *cs, const char *path, - DP_Rect *crop, int start, int end_inclusive, - int framerate, +DP_SaveResult DP_save_animation_gif(DP_CanvasState *cs, DP_DrawContext *dc, + const char *path, DP_Rect *crop, int width, + int height, int interpolation, int start, + int end_inclusive, int framerate, DP_SaveAnimationProgressFn progress_fn, void *user); diff --git a/src/drawdance/libimpex/dpimpex/save_video.c b/src/drawdance/libimpex/dpimpex/save_video.c index c3db14d768..24a4215261 100644 --- a/src/drawdance/libimpex/dpimpex/save_video.c +++ b/src/drawdance/libimpex/dpimpex/save_video.c @@ -87,8 +87,10 @@ static int get_format_loops(int format, int loops) } } -static int get_format_dimension(int format, int dimension) +static int get_format_dimension(int format, int target_dimension, + int input_dimension) { + int dimension = target_dimension > 0 ? target_dimension : input_dimension; switch (format) { case DP_SAVE_VIDEO_FORMAT_MP4: // H264 dimensions must be divisible by 2. @@ -98,6 +100,22 @@ static int get_format_dimension(int format, int dimension) } } +static int get_scaling_flags(unsigned int flags, int input_width, + int input_height, int output_width, + int output_height) +{ + // Only scale smoothly if it was requested and the difference is notable. A + // 1 pixel difference may be due to get_format_dimensions padding for H264. + if ((flags & DP_SAVE_VIDEO_FLAGS_SCALE_SMOOTH) + && abs(input_width - output_width) > 1 + && abs(input_height - output_height) > 1) { + return SWS_BILINEAR; + } + else { + return 0; + } +} + static int get_format_pix_fmt(int format) { switch (format) { @@ -301,7 +319,8 @@ DP_SaveResult DP_save_animation_video(DP_SaveVideoParams params) const char *format_name; enum AVCodecID codec_id; bool params_ok = - params.cs && params.path && params.path[0] + params.cs && params.path && params.path[0] && params.width > 0 + && params.height > 0 && DP_rect_valid(crop = get_crop(params.cs, params.area)) && (format_name = get_format_name(params.format)) && (codec_id = get_format_codec_id(params.format)) != AV_CODEC_ID_NONE; @@ -355,8 +374,10 @@ DP_SaveResult DP_save_animation_video(DP_SaveVideoParams params) int input_width = DP_rect_width(crop); int input_height = DP_rect_height(crop); - int output_width = get_format_dimension(params.format, input_width); - int output_height = get_format_dimension(params.format, input_height); + int output_width = + get_format_dimension(params.format, params.width, input_width); + int output_height = + get_format_dimension(params.format, params.height, input_height); codec_context->width = output_width; codec_context->height = output_height; codec_context->pix_fmt = get_format_pix_fmt(params.format); @@ -467,9 +488,12 @@ DP_SaveResult DP_save_animation_video(DP_SaveVideoParams params) goto cleanup; } - sws_context = - sws_getContext(input_width, input_height, AV_PIX_FMT_BGRA, output_width, - output_height, frame->format, 0, NULL, NULL, NULL); + sws_context = sws_getContext(input_width, input_height, AV_PIX_FMT_BGRA, + output_width, output_height, frame->format, + get_scaling_flags(params.flags, input_width, + input_height, output_width, + output_height), + NULL, NULL, NULL); if (!sws_context) { DP_error_set("Failed to allocate scaling context"); result = DP_SAVE_RESULT_INTERNAL_ERROR; diff --git a/src/drawdance/libimpex/dpimpex/save_video.h b/src/drawdance/libimpex/dpimpex/save_video.h index 6ae8af8a49..a55ab4e179 100644 --- a/src/drawdance/libimpex/dpimpex/save_video.h +++ b/src/drawdance/libimpex/dpimpex/save_video.h @@ -8,6 +8,9 @@ typedef struct DP_CanvasState DP_CanvasState; typedef struct DP_DrawContext DP_DrawContext; typedef struct DP_Rect DP_Rect; +#define DP_SAVE_VIDEO_FLAGS_NONE 0x0u +#define DP_SAVE_VIDEO_FLAGS_SCALE_SMOOTH = 0x1u + typedef enum DP_SaveVideoFormat { DP_SAVE_VIDEO_FORMAT_MP4, DP_SAVE_VIDEO_FORMAT_WEBM, @@ -18,7 +21,10 @@ typedef struct DP_SaveVideoParams { DP_CanvasState *cs; const DP_Rect *area; const char *path; + unsigned int flags; int format; + int width; + int height; int start; int end_inclusive; int framerate; diff --git a/src/drawdance/rust/bindings.rs b/src/drawdance/rust/bindings.rs index 4ab9d975bd..8e3fc3d186 100644 --- a/src/drawdance/rust/bindings.rs +++ b/src/drawdance/rust/bindings.rs @@ -2423,6 +2423,15 @@ extern "C" { out_thumb: *mut *mut DP_Image, ) -> bool; } +extern "C" { + pub fn DP_image_scale( + img: *mut DP_Image, + dc: *mut DP_DrawContext, + width: ::std::os::raw::c_int, + height: ::std::os::raw::c_int, + interpolation: ::std::os::raw::c_int, + ) -> *mut DP_Image; +} extern "C" { pub fn DP_image_same_pixel(img: *mut DP_Image, out_pixel: *mut DP_Pixel8) -> bool; } @@ -7946,8 +7955,12 @@ pub type DP_SaveAnimationProgressFn = ::std::option::Option< extern "C" { pub fn DP_save_animation_frames( cs: *mut DP_CanvasState, + dc: *mut DP_DrawContext, path: *const ::std::os::raw::c_char, crop: *mut DP_Rect, + width: ::std::os::raw::c_int, + height: ::std::os::raw::c_int, + interpolation: ::std::os::raw::c_int, start: ::std::os::raw::c_int, end_inclusive: ::std::os::raw::c_int, progress_fn: DP_SaveAnimationProgressFn, @@ -7957,8 +7970,12 @@ extern "C" { extern "C" { pub fn DP_save_animation_zip( cs: *mut DP_CanvasState, + dc: *mut DP_DrawContext, path: *const ::std::os::raw::c_char, crop: *mut DP_Rect, + width: ::std::os::raw::c_int, + height: ::std::os::raw::c_int, + interpolation: ::std::os::raw::c_int, start: ::std::os::raw::c_int, end_inclusive: ::std::os::raw::c_int, progress_fn: DP_SaveAnimationProgressFn, @@ -7968,8 +7985,12 @@ extern "C" { extern "C" { pub fn DP_save_animation_gif( cs: *mut DP_CanvasState, + dc: *mut DP_DrawContext, path: *const ::std::os::raw::c_char, crop: *mut DP_Rect, + width: ::std::os::raw::c_int, + height: ::std::os::raw::c_int, + interpolation: ::std::os::raw::c_int, start: ::std::os::raw::c_int, end_inclusive: ::std::os::raw::c_int, framerate: ::std::os::raw::c_int, diff --git a/src/libclient/export/animationsaverrunnable.cpp b/src/libclient/export/animationsaverrunnable.cpp index 3f32beb1c7..dbff04b8bc 100644 --- a/src/libclient/export/animationsaverrunnable.cpp +++ b/src/libclient/export/animationsaverrunnable.cpp @@ -6,6 +6,7 @@ extern "C" { # include #endif } +#include "libclient/drawdance/global.h" #include "libclient/export/animationformat.h" #include "libclient/export/animationsaverrunnable.h" #include "libclient/export/canvassaverrunnable.h" @@ -18,19 +19,23 @@ AnimationSaverRunnable::AnimationSaverRunnable( #ifndef __EMSCRIPTEN__ const QString &path, #endif - int format, int loops, int start, int end, int framerate, const QRect &crop, + int format, int width, int height, int loops, int start, int end, + int framerate, const QRect &crop, bool scaleSmooth, const drawdance::CanvasState &canvasState, QObject *parent) : QObject(parent) #ifndef __EMSCRIPTEN__ , m_path(path) #endif , m_format(format) + , m_width(width) + , m_height(height) , m_loops(loops) , m_start(start) , m_end(end) , m_framerate(framerate) , m_crop(crop) , m_canvasState(canvasState) + , m_scaleSmooth(scaleSmooth) , m_cancelled(false) { } @@ -59,22 +64,37 @@ void AnimationSaverRunnable::run() DP_SaveResult result; switch(m_format) { #if !defined(__EMSCRIPTEN__) && !defined(Q_OS_ANDROID) - case int(AnimationFormat::Frames): + case int(AnimationFormat::Frames): { + drawdance::DrawContext dc = drawdance::DrawContextPool::acquire(); result = DP_save_animation_frames( - m_canvasState.get(), pathBytes.constData(), pr, m_start, m_end, - &onProgress, this); + m_canvasState.get(), dc.get(), pathBytes.constData(), pr, m_width, + m_height, + m_scaleSmooth ? DP_MSG_TRANSFORM_REGION_MODE_BILINEAR + : DP_MSG_TRANSFORM_REGION_MODE_NEAREST, + m_start, m_end, &onProgress, this); break; + } #endif - case int(AnimationFormat::Zip): + case int(AnimationFormat::Zip): { + drawdance::DrawContext dc = drawdance::DrawContextPool::acquire(); result = DP_save_animation_zip( - m_canvasState.get(), pathBytes.constData(), pr, m_start, m_end, - &onProgress, this); + m_canvasState.get(), dc.get(), pathBytes.constData(), pr, m_width, + m_height, + m_scaleSmooth ? DP_MSG_TRANSFORM_REGION_MODE_BILINEAR + : DP_MSG_TRANSFORM_REGION_MODE_NEAREST, + m_start, m_end, &onProgress, this); break; - case int(AnimationFormat::Gif): + } + case int(AnimationFormat::Gif): { + drawdance::DrawContext dc = drawdance::DrawContextPool::acquire(); result = DP_save_animation_gif( - m_canvasState.get(), pathBytes.constData(), pr, m_start, m_end, - m_framerate, &onProgress, this); + m_canvasState.get(), dc.get(), pathBytes.constData(), pr, m_width, + m_height, + m_scaleSmooth ? DP_MSG_TRANSFORM_REGION_MODE_BILINEAR + : DP_MSG_TRANSFORM_REGION_MODE_NEAREST, + m_start, m_end, m_framerate, &onProgress, this); break; + } #ifdef DP_LIBAV case int(AnimationFormat::Webp): case int(AnimationFormat::Mp4): @@ -83,7 +103,11 @@ void AnimationSaverRunnable::run() m_canvasState.get(), pr, pathBytes.constData(), + m_scaleSmooth ? DP_SAVE_VIDEO_FLAGS_SCALE_SMOOTH + : DP_SAVE_VIDEO_FLAGS_NONE, formatToSaveVideoFormat(), + m_width, + m_height, m_start, m_end, m_framerate, diff --git a/src/libclient/export/animationsaverrunnable.h b/src/libclient/export/animationsaverrunnable.h index d3be652a39..5cc460eb62 100644 --- a/src/libclient/export/animationsaverrunnable.h +++ b/src/libclient/export/animationsaverrunnable.h @@ -19,9 +19,9 @@ class AnimationSaverRunnable final : public QObject, public QRunnable { #ifndef __EMSCRIPTEN__ const QString &path, #endif - int format, int loops, int start, int end, int framerate, - const QRect &crop, const drawdance::CanvasState &canvasState, - QObject *parent = nullptr); + int format, int width, int height, int loops, int start, int end, + int framerate, const QRect &crop, bool scaleSmooth, + const drawdance::CanvasState &canvasState, QObject *parent = nullptr); void run() override; @@ -50,12 +50,15 @@ public slots: const QString m_path; #endif const int m_format; + const int m_width; + const int m_height; const int m_loops; const int m_start; const int m_end; const int m_framerate; const QRect m_crop; const drawdance::CanvasState m_canvasState; + const bool m_scaleSmooth; bool m_cancelled; };