Skip to content

Commit

Permalink
Implement Drawpile 2.1 animation import
Browse files Browse the repository at this point in the history
Imports an ORA file and reconstitutes it like Drawpile 2.1 would have
animated it. Fixed layers get turned into tracks with a single frame at
the beginning, layers get put into groups and put on matching tracks.
Usually an animation will have one such group and track, unless fixed
layers are used in the middle, in which case they get split up into
multiple tracks so that the layering works correctly.

This doesn't check if the ORA file being imported actually came from
Drawpile 2.1, since it doesn't matter for the functionality.
  • Loading branch information
askmeaboutlo0m committed Oct 29, 2023
1 parent 8f57c87 commit 8c84f3f
Show file tree
Hide file tree
Showing 41 changed files with 3,150 additions and 48 deletions.
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Unreleased Version 2.2.0-pre
* Fix: Don't lock tool slots when using the eraser tool, to avoid accidentally getting stuck in it.
* Feature: Translate a single-colored bottom layer into background color when loading ORA and PSD files.
* Fix: Make Alt+Space canvas shortcut sorta work in Windows. Thanks Bovy and xxxx for reporting.
* Feature: Drawpile 2.1 animation import.

2023-09-30 Version 2.2.0-beta.8
* Fix: Apply color wheel direction to color dialogs too. Thanks Blozzom for reporting.
Expand Down
2 changes: 2 additions & 0 deletions src/desktop/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ target_sources(drawpile PRIVATE
dialogs/abusereport.h
dialogs/addserverdialog.cpp
dialogs/addserverdialog.h
dialogs/animationimportdialog.cpp
dialogs/animationimportdialog.h
dialogs/avatarimport.cpp
dialogs/avatarimport.h
dialogs/brushexportdialog.cpp
Expand Down
140 changes: 140 additions & 0 deletions src/desktop/dialogs/animationimportdialog.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// SPDX-License-Identifier: GPL-3.0-or-later
extern "C" {
#include <dpengine/canvas_state.h>
}
#include "desktop/dialogs/animationimportdialog.h"
#include "desktop/filewrangler.h"
#include "desktop/utils/widgetutils.h"
#include "libclient/import/animationimporter.h"
#include <QApplication>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QIcon>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QSpinBox>
#include <QThreadPool>
#include <QVBoxLayout>

namespace dialogs {

AnimationImportDialog::AnimationImportDialog(QWidget *parent)
: QDialog(parent)
{
setModal(true);
setWindowTitle(tr("Import Animation"));
resize(600, 200);

QVBoxLayout *layout = new QVBoxLayout;
setLayout(layout);

QFormLayout *form = new QFormLayout;
layout->addLayout(form);

QHBoxLayout *pathLayout = new QHBoxLayout;
form->addRow(tr("File to Import:"), pathLayout);

m_pathEdit = new QLineEdit;
pathLayout->addWidget(m_pathEdit);

m_chooseButton = new QPushButton(tr("Choose"));
pathLayout->addWidget(m_chooseButton);
connect(
m_chooseButton, &QAbstractButton::clicked, this,
&AnimationImportDialog::chooseFile);

m_holdTime = new QSpinBox;
m_holdTime->setRange(1, 99);
m_holdTime->setValue(1);
//: How many frames each imported key frame gets in the timeline.
form->addRow(tr("Key frame length:"), m_holdTime);
updateHoldTimeSuffix(m_holdTime->value());
connect(
m_holdTime, QOverload<int>::of(&QSpinBox::valueChanged), this,
&AnimationImportDialog::updateHoldTimeSuffix);

m_framerate = new QSpinBox;
m_framerate->setRange(1, 999);
m_framerate->setValue(24);
form->addRow(tr("Framerate:"), m_framerate);

layout->addStretch();

m_buttons = new QDialogButtonBox(QDialogButtonBox::Cancel);
layout->addWidget(m_buttons);
connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);

m_importButton =
m_buttons->addButton(tr("Import"), QDialogButtonBox::ActionRole);
m_importButton->setIcon(QIcon::fromTheme("document-import"));
connect(
m_buttons, &QDialogButtonBox::clicked, this,
&AnimationImportDialog::buttonClicked);
connect(
m_pathEdit, &QLineEdit::textChanged, this,
&AnimationImportDialog::updateImportButton);
updateImportButton(m_pathEdit->text());
}

AnimationImportDialog::~AnimationImportDialog()
{
if(!isEnabled()) {
QApplication::restoreOverrideCursor();
}
}

void AnimationImportDialog::chooseFile()
{
QString path = FileWrangler(this).getOpenOraPath();
if(!path.isEmpty()) {
m_pathEdit->setText(path);
}
}

void AnimationImportDialog::updateHoldTimeSuffix(int value)
{
m_holdTime->setSuffix(tr(" frame(s)", "", value));
}

void AnimationImportDialog::updateImportButton(const QString &path)
{
m_importButton->setEnabled(!path.trimmed().isEmpty());
}

void AnimationImportDialog::buttonClicked(QAbstractButton *button)
{
if(button == m_importButton) {
runImport();
}
}

void AnimationImportDialog::importFinished(
const drawdance::CanvasState &canvasState, const QString &error)
{
if(canvasState.isNull()) {
QApplication::restoreOverrideCursor();
QMessageBox::critical(this, tr("Animation Import Error"), error);
setEnabled(true);
} else {
emit canvasStateImported(canvasState);
}
}

void AnimationImportDialog::runImport()
{
if(isEnabled()) {
impex::AnimationImporter *importer = new impex::AnimationImporter(
m_pathEdit->text().trimmed(), m_holdTime->value(),
m_framerate->value());
connect(
importer, &impex::AnimationImporter::finished, this,
&AnimationImportDialog::importFinished);
QThreadPool::globalInstance()->start(importer);
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
setEnabled(false);
}
}

}
45 changes: 45 additions & 0 deletions src/desktop/dialogs/animationimportdialog.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef DESKTOP_DIALOGS_ANIMATIONIMPORTDIALOG
#define DESKTOP_DIALOGS_ANIMATIONIMPORTDIALOG
#include "libclient/drawdance/canvasstate.h"
#include <QDialog>

class QAbstractButton;
class QDialogButtonBox;
class QLineEdit;
class QPushButton;
class QSpinBox;

namespace dialogs {

class AnimationImportDialog final : public QDialog {
Q_OBJECT
public:
explicit AnimationImportDialog(QWidget *parent = nullptr);
~AnimationImportDialog() override;

signals:
void canvasStateImported(const drawdance::CanvasState &canvasState);

private slots:
void chooseFile();
void updateHoldTimeSuffix(int value);
void updateImportButton(const QString &path);
void buttonClicked(QAbstractButton *button);
void importFinished(
const drawdance::CanvasState &canvasState, const QString &error);

private:
QLineEdit *m_pathEdit;
QPushButton *m_chooseButton;
QSpinBox *m_holdTime;
QSpinBox *m_framerate;
QDialogButtonBox *m_buttons;
QPushButton *m_importButton;

void runImport();
};

}

#endif
17 changes: 15 additions & 2 deletions src/desktop/filewrangler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ QString FileWrangler::getOpenPath() const
tr("Open"), LastPath::IMAGE, utils::FileFormatOption::OpenEverything);
}

QString FileWrangler::getOpenOraPath() const
{
return showOpenFileDialogFilter(
tr("Open ORA"), LastPath::IMAGE,
QStringLiteral("%1 (*.ora)").arg(tr("OpenRaster Image")));
}

QString FileWrangler::getOpenPasteImagePath() const
{
return showOpenFileDialog(
Expand Down Expand Up @@ -413,10 +420,16 @@ QString FileWrangler::getDefaultLastPath(LastPath type, const QString &ext)

QString FileWrangler::showOpenFileDialog(
const QString &title, LastPath type, utils::FileFormatOptions formats) const
{
return showOpenFileDialogFilter(
title, type, utils::fileFormatFilter(formats));
}

QString FileWrangler::showOpenFileDialogFilter(
const QString &title, LastPath type, const QString &filter) const
{
QString filename = QFileDialog::getOpenFileName(
parentWidget(), title, getLastPath(type),
utils::fileFormatFilter(formats));
parentWidget(), title, getLastPath(type), filter);
if(filename.isEmpty()) {
return QString{};
} else {
Expand Down
4 changes: 4 additions & 0 deletions src/desktop/filewrangler.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class FileWrangler final : public QObject {

QStringList getImportCertificatePaths(const QString &title) const;
QString getOpenPath() const;
QString getOpenOraPath() const;
QString getOpenPasteImagePath() const;
QString getOpenDebugDumpsPath() const;
QString getOpenBrushPackPath() const;
Expand Down Expand Up @@ -94,6 +95,9 @@ class FileWrangler final : public QObject {
const QString &title, LastPath type,
utils::FileFormatOptions formats) const;

QString showOpenFileDialogFilter(
const QString &title, LastPath type, const QString &filter) const;

QString showSaveFileDialog(
const QString &title, LastPath type, const QString &ext,
utils::FileFormatOptions formats, QString *selectedFilter = nullptr,
Expand Down
64 changes: 28 additions & 36 deletions src/desktop/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ static constexpr auto CTRL_KEY = Qt::CTRL;
#include "desktop/dialogs/sessionundodepthlimitdialog.h"
#include "desktop/dialogs/userinfodialog.h"
#include "desktop/dialogs/startdialog.h"
#include "desktop/dialogs/animationimportdialog.h"
#include "libclient/import/loadresult.h"

#ifdef Q_OS_WIN
#include "desktop/bundled/kis_tablet/kis_tablet_support_win.h"
Expand Down Expand Up @@ -1467,6 +1469,22 @@ void MainWindow::exportImage()
}
}

void MainWindow::importOldAnimation()
{
dialogs::AnimationImportDialog *dlg =
new dialogs::AnimationImportDialog(this);
dlg->setAttribute(Qt::WA_DeleteOnClose);
connect(
dlg, &dialogs::AnimationImportDialog::canvasStateImported, this,
[this, dlg](const drawdance::CanvasState &canvasState) {
// Don't use the path of the imported animation to avoid clobbering
// of the old file by mashing Ctrl+S instinctually.
replaceableWindow()->m_doc->loadState(canvasState, QString(), true);
dlg->deleteLater();
});
utils::showWindow(dlg);
}

void MainWindow::onCanvasSaveStarted()
{
QApplication::setOverrideCursor(QCursor(Qt::BusyCursor));
Expand Down Expand Up @@ -2352,42 +2370,13 @@ void MainWindow::showErrorMessageWithDetails(const QString &message, const QStri

void MainWindow::showLoadResultMessage(DP_LoadResult result)
{
switch(result) {
case DP_LOAD_RESULT_SUCCESS:
break;
case DP_LOAD_RESULT_BAD_ARGUMENTS:
showErrorMessage(tr("Bad arguments, this is probably a bug in Drawpile."));
break;
case DP_LOAD_RESULT_UNKNOWN_FORMAT:
showErrorMessage(tr("Unsupported format."));
break;
case DP_LOAD_RESULT_OPEN_ERROR:
showErrorMessageWithDetails(tr("Couldn't open file for reading."), DP_error());
break;
case DP_LOAD_RESULT_READ_ERROR:
showErrorMessageWithDetails(tr("Error reading file."), DP_error());
break;
case DP_LOAD_RESULT_BAD_MIMETYPE:
showErrorMessage(tr("File content doesn't match its type."));
break;
case DP_LOAD_RESULT_RECORDING_INCOMPATIBLE:
showErrorMessage(tr("Incompatible recording."));
break;
case DP_LOAD_RESULT_UNSUPPORTED_PSD_BITS_PER_CHANNEL:
showErrorMessage(tr("Unsupported bits per channel. Only 8 bits are supported."));
break;
case DP_LOAD_RESULT_UNSUPPORTED_PSD_COLOR_MODE:
showErrorMessage(tr("Unsupported color mode. Only RGB/RGBA is supported."));
break;
case DP_LOAD_RESULT_IMAGE_TOO_LARGE:
showErrorMessage(tr("Image dimensions are too large."));
break;
case DP_LOAD_RESULT_INTERNAL_ERROR:
showErrorMessage(tr("Internal error, this is probably a bug."));
break;
default:
showErrorMessageWithDetails(tr("Unknown error."), DP_error());
break;
if(result != DP_LOAD_RESULT_SUCCESS) {
QString message = impex::getLoadResultMessage(result);
if(impex::shouldIncludeLoadResultDpError(result)) {
showErrorMessageWithDetails(message, DP_error());
} else {
showErrorMessage(message);
}
}
}

Expand Down Expand Up @@ -3256,6 +3245,7 @@ void MainWindow::setupActions()
QAction *exportDocument = makeAction("exportdocument", tr("Export Image…")).icon("document-export").noDefaultShortcut();
QAction *savesel = makeAction("saveselection", tr("Export Selection...")).icon("select-rectangular").noDefaultShortcut();
QAction *autosave = makeAction("autosave", tr("Autosave")).noDefaultShortcut().checkable().disabled();
QAction *importOldAnimation = makeAction("importoldanimation", tr("Import &Drawpile 2.1 Animation…")).noDefaultShortcut();
QAction *importBrushes = makeAction("importbrushes", tr("Import &Brushes...")).noDefaultShortcut();
QAction *exportTemplate = makeAction("exporttemplate", tr("Export Session &Template...")).noDefaultShortcut();
QAction *exportGifAnimation = makeAction("exportanimgif", tr("Export Animated &GIF...")).noDefaultShortcut();
Expand Down Expand Up @@ -3288,6 +3278,7 @@ void MainWindow::setupActions()
connect(saveas, SIGNAL(triggered()), this, SLOT(saveas()));
connect(exportDocument, &QAction::triggered, this, &MainWindow::exportImage);
connect(exportTemplate, &QAction::triggered, this, &MainWindow::exportTemplate);
connect(importOldAnimation, &QAction::triggered, this, &MainWindow::importOldAnimation);
connect(importBrushes, &QAction::triggered, m_dockBrushPalette, &docks::BrushPalette::importBrushes);
connect(exportBrushes, &QAction::triggered, m_dockBrushPalette, &docks::BrushPalette::exportBrushes);
connect(savesel, &QAction::triggered, this, &MainWindow::saveSelection);
Expand Down Expand Up @@ -3329,6 +3320,7 @@ void MainWindow::setupActions()

QMenu *importMenu = filemenu->addMenu(tr("&Import"));
importMenu->setIcon(QIcon::fromTheme("document-import"));
importMenu->addAction(importOldAnimation);
importMenu->addAction(importBrushes);

QMenu *exportMenu = filemenu->addMenu(tr("&Export"));
Expand Down
1 change: 1 addition & 0 deletions src/desktop/mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public slots:
void saveas();
void saveSelection();
void exportImage();
void importOldAnimation();
void showFlipbook();

void showBrushSettingsDialog();
Expand Down
2 changes: 2 additions & 0 deletions src/drawdance/libengine/cbindgen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ renaming_overrides_prefixing = true
[export.rename]
"DP_CanvasState" = "DP_CanvasState"
"DP_DrawContext" = "DP_DrawContext"
"DP_LoadResult" = "DP_LoadResult"
"DP_SaveResult" = "DP_SaveResult"
"DP_TransientLayerProps" = "DP_TransientLayerProps"
"DP_TransientTrack" = "DP_TransientTrack"

[enum]
rename_variants = "QualifiedScreamingSnakeCase"
10 changes: 10 additions & 0 deletions src/drawdance/libengine/dpengine/canvas_state.c
Original file line number Diff line number Diff line change
Expand Up @@ -2022,6 +2022,16 @@ void DP_transient_canvas_state_timeline_set_inc(DP_TransientCanvasState *tcs,
tcs->timeline = DP_timeline_incref(tl);
}

void DP_transient_canvas_state_transient_timeline_set_noinc(
DP_TransientCanvasState *tcs, DP_TransientTimeline *ttl)
{
DP_ASSERT(tcs);
DP_ASSERT(DP_atomic_get(&tcs->refcount) > 0);
DP_ASSERT(tcs->transient);
DP_timeline_decref(tcs->timeline);
tcs->transient_timeline = ttl;
}

DP_TransientDocumentMetadata *
DP_transient_canvas_state_transient_metadata(DP_TransientCanvasState *tcs)
{
Expand Down
Loading

0 comments on commit 8c84f3f

Please sign in to comment.