diff --git a/app/src/actioncommands.cpp b/app/src/actioncommands.cpp index 35727c69e..347779eeb 100644 --- a/app/src/actioncommands.cpp +++ b/app/src/actioncommands.cpp @@ -47,6 +47,8 @@ GNU General Public License for more details. #include "soundclip.h" #include "camera.h" +#include "importimageseqdialog.h" +#include "importpositiondialog.h" #include "movieimporter.h" #include "movieexporter.h" #include "filedialog.h" @@ -65,6 +67,50 @@ ActionCommands::ActionCommands(QWidget* parent) : QObject(parent) ActionCommands::~ActionCommands() {} +Status ActionCommands::importAnimatedImage() +{ + ImportImageSeqDialog fileDialog(mParent, ImportExportDialog::Import, FileType::ANIMATED_IMAGE); + fileDialog.exec(); + if (fileDialog.result() != QDialog::Accepted) + { + return Status::CANCELED; + } + int frameSpacing = fileDialog.getSpace(); + QString strImgFileLower = fileDialog.getFilePath(); + + ImportPositionDialog positionDialog(mEditor, mParent); + positionDialog.exec(); + if (positionDialog.result() != QDialog::Accepted) + { + return Status::CANCELED; + } + + // Show a progress dialog, as this could take a while if the gif is huge + QProgressDialog progressDialog(tr("Importing Animated Image..."), tr("Abort"), 0, 100, mParent); + hideQuestionMark(progressDialog); + progressDialog.setWindowModality(Qt::WindowModal); + progressDialog.show(); + + Status st = mEditor->importAnimatedImage(strImgFileLower, frameSpacing, [&progressDialog](int prog) { + progressDialog.setValue(prog); + QApplication::processEvents(); + }, [&progressDialog]() { + return progressDialog.wasCanceled(); + }); + + progressDialog.setValue(100); + progressDialog.close(); + + if (!st.ok()) + { + ErrorDialog errorDialog(st.title(), st.description(), st.details().html()); + errorDialog.exec(); + return Status::SAFE; + } + + return Status::OK; +} + Status ActionCommands::importMovieVideo() { QString filePath = FileDialog::getOpenFileName(mParent, FileType::MOVIE); @@ -106,6 +152,7 @@ Status ActionCommands::importMovieVideo() { ErrorDialog errorDialog(st.title(), st.description(), st.details().html(), mParent); errorDialog.exec(); + return Status::SAFE; } mEditor->layers()->notifyAnimationLengthChanged(); @@ -293,7 +340,7 @@ Status ActionCommands::exportMovie(bool isGif) desc.loop = dialog->getLoop(); desc.alpha = dialog->getTransparency(); - DoubleProgressDialog progressDlg; + DoubleProgressDialog progressDlg(mParent); progressDlg.setWindowModality(Qt::WindowModal); progressDlg.setWindowTitle(tr("Exporting movie")); Qt::WindowFlags eFlags = Qt::Dialog | Qt::WindowTitleHint; diff --git a/app/src/actioncommands.h b/app/src/actioncommands.h index f55e832c8..6d8ac3e34 100644 --- a/app/src/actioncommands.h +++ b/app/src/actioncommands.h @@ -37,6 +37,7 @@ class ActionCommands : public QObject void setCore(Editor* e) { mEditor = e; } // file + Status importAnimatedImage(); Status importMovieVideo(); Status importSound(FileType type); Status exportMovie(bool isGif = false); diff --git a/app/src/doubleprogressdialog.cpp b/app/src/doubleprogressdialog.cpp index 7d171a01b..22c2dc390 100644 --- a/app/src/doubleprogressdialog.cpp +++ b/app/src/doubleprogressdialog.cpp @@ -21,7 +21,7 @@ GNU General Public License for more details. #include DoubleProgressDialog::DoubleProgressDialog(QWidget *parent) : - QDialog(parent), + QProgressDialog(parent), ui(new Ui::DoubleProgressDialog) { ui->setupUi(this); @@ -29,7 +29,7 @@ DoubleProgressDialog::DoubleProgressDialog(QWidget *parent) : major = new ProgressBarControl(ui->majorProgressBar); minor = new ProgressBarControl(ui->minorProgressBar); - connect(ui->cancelButton, &QPushButton::pressed, this, &DoubleProgressDialog::canceled); + setCancelButton(ui->cancelButton); } DoubleProgressDialog::~DoubleProgressDialog() diff --git a/app/src/doubleprogressdialog.h b/app/src/doubleprogressdialog.h index 8dda8301f..93bfb67ab 100644 --- a/app/src/doubleprogressdialog.h +++ b/app/src/doubleprogressdialog.h @@ -18,14 +18,14 @@ GNU General Public License for more details. #ifndef DOUBLEPROGRESSDIALOG_H #define DOUBLEPROGRESSDIALOG_H -#include +#include #include namespace Ui { class DoubleProgressDialog; } -class DoubleProgressDialog : public QDialog +class DoubleProgressDialog : public QProgressDialog { Q_OBJECT @@ -63,9 +63,6 @@ class DoubleProgressDialog : public QDialog ProgressBarControl *major, *minor; -signals: - void canceled(); - private: Ui::DoubleProgressDialog *ui; }; diff --git a/app/src/filedialog.cpp b/app/src/filedialog.cpp index 8b35d69ca..42ee65db2 100644 --- a/app/src/filedialog.cpp +++ b/app/src/filedialog.cpp @@ -119,6 +119,7 @@ QString FileDialog::getDefaultExtensionByFileType(const FileType fileType) case FileType::IMAGE: return PFF_DEFAULT_IMAGE_EXT; case FileType::IMAGE_SEQUENCE: return PFF_DEFAULT_IMAGE_SEQ_EXT; case FileType::GIF: return PFF_DEFAULT_ANIMATED_EXT; + case FileType::ANIMATED_IMAGE: return PFF_DEFAULT_ANIMATED_EXT; case FileType::PALETTE: return PFF_DEFAULT_PALETTE_EXT; case FileType::MOVIE: return PFF_DEFAULT_MOVIE_EXT; case FileType::SOUND: return PFF_DEFAULT_SOUND_EXT; @@ -167,6 +168,7 @@ QString FileDialog::openDialogCaption(FileType fileType) case FileType::IMAGE: return tr("Import image"); case FileType::IMAGE_SEQUENCE: return tr("Import image sequence"); case FileType::GIF: return tr("Import Animated GIF"); + case FileType::ANIMATED_IMAGE: return tr("Import animated image"); case FileType::MOVIE: return tr("Import movie"); case FileType::SOUND: return tr("Import sound"); case FileType::PALETTE: return tr("Open palette"); @@ -182,6 +184,7 @@ QString FileDialog::saveDialogCaption(FileType fileType) case FileType::IMAGE: return tr("Export image"); case FileType::IMAGE_SEQUENCE: return tr("Export image sequence"); case FileType::GIF: return tr("Export Animated GIF"); + case FileType::ANIMATED_IMAGE: return tr("Export animated image"); case FileType::MOVIE: return tr("Export movie"); case FileType::SOUND: return tr("Export sound"); case FileType::PALETTE: return tr("Export palette"); @@ -197,6 +200,7 @@ QString FileDialog::openFileFilters(FileType fileType) case FileType::IMAGE: return PFF_IMAGE_FILTER; case FileType::IMAGE_SEQUENCE: return PFF_IMAGE_SEQ_FILTER; case FileType::GIF: return PFF_GIF_EXT_FILTER; + case FileType::ANIMATED_IMAGE: return PFF_ANIMATED_IMAGE_EXT_FILTER; case FileType::MOVIE: return PFF_MOVIE_EXT; case FileType::SOUND: return PFF_SOUND_EXT_FILTER; case FileType::PALETTE: return PFF_PALETTE_EXT_FILTER; @@ -212,6 +216,7 @@ QString FileDialog::saveFileFilters(FileType fileType) case FileType::IMAGE: return ""; case FileType::IMAGE_SEQUENCE: return ""; case FileType::GIF: return QString("%1 (*.gif)").arg(tr("Animated GIF")); + case FileType::ANIMATED_IMAGE: return ""; case FileType::MOVIE: return "MP4 (*.mp4);; AVI (*.avi);; WebM (*.webm);; APNG (*.apng)"; case FileType::SOUND: return ""; case FileType::PALETTE: return PFF_PALETTE_EXT_FILTER; @@ -286,6 +291,7 @@ QString FileDialog::toSettingKey(FileType fileType) case FileType::IMAGE: return "Image"; case FileType::IMAGE_SEQUENCE: return "ImageSequence"; case FileType::GIF: return "Animated GIF"; + case FileType::ANIMATED_IMAGE: return "Animated Image"; case FileType::MOVIE: return "Movie"; case FileType::SOUND: return "Sound"; case FileType::PALETTE: return "Palette"; diff --git a/app/src/importimageseqdialog.cpp b/app/src/importimageseqdialog.cpp index 896067a88..86c451591 100644 --- a/app/src/importimageseqdialog.cpp +++ b/app/src/importimageseqdialog.cpp @@ -58,10 +58,16 @@ void ImportImageSeqDialog::setupLayout() hideInstructionsLabel(true); - if (mFileType == FileType::GIF) { + switch (mFileType) + { + case FileType::GIF: setWindowTitle(tr("Import Animated GIF")); - } else { + break; + case FileType::IMAGE_SEQUENCE: setWindowTitle(tr("Import image sequence")); + break; + default: + setWindowTitle(tr("Import animated image")); } connect(uiOptionsBox->spaceSpinBox, static_cast(&QSpinBox::valueChanged), this, &ImportImageSeqDialog::setSpace); diff --git a/app/src/mainwindow2.cpp b/app/src/mainwindow2.cpp index 81ca1189b..da3d6ee2b 100644 --- a/app/src/mainwindow2.cpp +++ b/app/src/mainwindow2.cpp @@ -254,7 +254,7 @@ void MainWindow2::createMenus() connect(ui->actionImport_ImageSeqNum, &QAction::triggered, this, &MainWindow2::importPredefinedImageSet); connect(ui->actionImportLayers_from_pclx, &QAction::triggered, this, &MainWindow2::importLayers); connect(ui->actionImport_MovieVideo, &QAction::triggered, this, &MainWindow2::importMovieVideo); - connect(ui->actionImport_Gif, &QAction::triggered, this, &MainWindow2::importGIF); + connect(ui->actionImport_AnimatedImage, &QAction::triggered, this, &MainWindow2::importAnimatedImage); connect(ui->actionImport_Sound, &QAction::triggered, [=] { mCommands->importSound(FileType::SOUND); }); connect(ui->actionImport_MovieAudio, &QAction::triggered, [=] { mCommands->importSound(FileType::MOVIE); }); @@ -949,57 +949,12 @@ void MainWindow2::importLayers() importLayers->open(); } -void MainWindow2::importGIF() +void MainWindow2::importAnimatedImage() { - auto gifDialog = new ImportImageSeqDialog(this, ImportExportDialog::Import, FileType::GIF); - gifDialog->exec(); - if (gifDialog->result() == QDialog::Rejected) - { - return; - } - // Flag this so we don't prompt the user about auto-save in the middle of the import. mSuppressAutoSaveDialog = true; - ImportPositionDialog* positionDialog = new ImportPositionDialog(mEditor, this); - OnScopeExit(delete positionDialog) - - positionDialog->exec(); - if (positionDialog->result() != QDialog::Accepted) - { - return; - } - - int space = gifDialog->getSpace(); - - // Show a progress dialog, as this could take a while if the gif is huge - QProgressDialog progress(tr("Importing Animated GIF..."), tr("Abort"), 0, 100, this); - hideQuestionMark(progress); - progress.setWindowModality(Qt::WindowModal); - progress.show(); - - QString strImgFileLower = gifDialog->getFilePath(); - if (!strImgFileLower.toLower().endsWith(".gif")) - { - ErrorDialog errorDialog(tr("Import failed"), tr("You can only import files ending with .gif.")); - errorDialog.exec(); - } - else - { - Status st = mEditor->importGIF(strImgFileLower, space); - - progress.setValue(50); - QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); // Required to make progress bar update - - progress.setValue(100); - progress.close(); - - if (!st.ok()) - { - ErrorDialog errorDialog(st.title(), st.description(), st.details().html()); - errorDialog.exec(); - } - } + mCommands->importAnimatedImage(); mSuppressAutoSaveDialog = false; } diff --git a/app/src/mainwindow2.h b/app/src/mainwindow2.h index a9f9f7a0c..9f1b683f6 100644 --- a/app/src/mainwindow2.h +++ b/app/src/mainwindow2.h @@ -90,7 +90,7 @@ public slots: void importPredefinedImageSet(); void importLayers(); void importMovieVideo(); - void importGIF(); + void importAnimatedImage(); void lockWidgets(bool shouldLock); diff --git a/app/ui/exportimageoptions.ui b/app/ui/exportimageoptions.ui index 23341ac93..98d1ae91c 100644 --- a/app/ui/exportimageoptions.ui +++ b/app/ui/exportimageoptions.ui @@ -98,6 +98,11 @@ TIFF + + + WEBP + + diff --git a/app/ui/mainwindow2.ui b/app/ui/mainwindow2.ui index 0d35271b7..d444be23e 100644 --- a/app/ui/mainwindow2.ui +++ b/app/ui/mainwindow2.ui @@ -65,7 +65,7 @@ - + @@ -912,9 +912,9 @@ F1 - + - Animated GIF... + Animated Image... diff --git a/core_lib/src/interface/editor.cpp b/core_lib/src/interface/editor.cpp index 7c4d5bc60..49af52acb 100644 --- a/core_lib/src/interface/editor.cpp +++ b/core_lib/src/interface/editor.cpp @@ -914,7 +914,7 @@ void Editor::updateObject() emit updateLayerCount(); } -Status Editor::importBitmapImage(const QString& filePath, int space) +Status Editor::importBitmapImage(const QString& filePath) { QImageReader reader(filePath); @@ -926,8 +926,7 @@ Status Editor::importBitmapImage(const QString& filePath, int space) dd << QString("Raw file path: %1").arg(filePath); QImage img(reader.size(), QImage::Format_ARGB32_Premultiplied); - if (img.isNull()) - { + if (!reader.read(&img)) { QString format = reader.format(); if (!format.isEmpty()) { @@ -955,33 +954,18 @@ Status Editor::importBitmapImage(const QString& filePath, int space) const QPoint pos(view()->getImportView().dx() - (img.width() / 2), view()->getImportView().dy() - (img.height() / 2)); - while (reader.read(&img)) + if (!layer->keyExists(mFrame)) { - int frameNumber = mFrame; - if (!layer->keyExists(frameNumber)) - { - addNewKey(); - } - BitmapImage* bitmapImage = layer->getBitmapImageAtFrame(frameNumber); - BitmapImage importedBitmapImage(pos, img); - bitmapImage->paste(&importedBitmapImage); - emit frameModified(bitmapImage->pos()); + addNewKey(); + } + BitmapImage* bitmapImage = layer->getBitmapImageAtFrame(mFrame); + BitmapImage importedBitmapImage(pos, img); + bitmapImage->paste(&importedBitmapImage); + emit frameModified(bitmapImage->pos()); - if (space > 1) { - frameNumber += space; - } else { - frameNumber += 1; - } - scrubTo(frameNumber); + scrubTo(mFrame+1); - backup(tr("Import Image")); - - // Workaround for tiff import getting stuck in this loop - if (!reader.supportsAnimation()) - { - break; - } - } + backup(tr("Import Image")); return status; } @@ -1048,17 +1032,76 @@ Status Editor::importImage(const QString& filePath) } } -Status Editor::importGIF(const QString& filePath, int numOfImages) +Status Editor::importAnimatedImage(const QString& filePath, int frameSpacing, const std::function& progressChanged, const std::function& wasCanceled) { + frameSpacing = qMax(1, frameSpacing); + + DebugDetails dd; + dd << QString("Raw file path: %1").arg(filePath); + Layer* layer = layers()->currentLayer(); if (layer->type() != Layer::BITMAP) { - DebugDetails dd; - dd << QString("Raw file path: %1").arg(filePath); dd << QString("Current layer: %1").arg(layer->type()); return Status(Status::ERROR_INVALID_LAYER_TYPE, dd, tr("Import failed"), tr("You can only import images to a bitmap layer.")); } - return importBitmapImage(filePath, numOfImages); + LayerBitmap* bitmapLayer = static_cast(layers()->currentLayer()); + + QImageReader reader(filePath); + dd << QString("QImageReader format: %1").arg(QString(reader.format())); + if (!reader.supportsAnimation()) { + return Status(Status::ERROR_INVALID_LAYER_TYPE, dd, tr("Import failed"), tr("The selected image has a format that does not support animation.")); + } + + QImage img(reader.size(), QImage::Format_ARGB32_Premultiplied); + const QPoint pos(view()->getImportView().dx() - (img.width() / 2), + view()->getImportView().dy() - (img.height() / 2)); + int totalFrames = reader.imageCount(); + while (reader.read(&img)) + { + if (reader.error()) + { + dd << QString("QImageReader ImageReaderError type: %1").arg(reader.errorString()); + + QString errorDesc; + switch(reader.error()) + { + case QImageReader::ImageReaderError::FileNotFoundError: + errorDesc = tr("File not found at path \"%1\". Please check the image is present at the specified location and try again.").arg(filePath); + break; + case QImageReader::UnsupportedFormatError: + errorDesc = tr("Image format is not supported. Please convert the image file to one of the following formats and try again:\n%1") + .arg((QString)reader.supportedImageFormats().join(", ")); + break; + default: + errorDesc = tr("An error has occurred while reading the image. Please check that the file is a valid image and try again."); + } + + return Status(Status::FAIL, dd, tr("Import failed"), errorDesc); + } + + if (!bitmapLayer->keyExists(mFrame)) + { + addNewKey(); + } + BitmapImage* bitmapImage = bitmapLayer->getBitmapImageAtFrame(mFrame); + BitmapImage importedBitmapImage(pos, img); + bitmapImage->paste(&importedBitmapImage); + emit frameModified(bitmapImage->pos()); + + if (wasCanceled()) + { + break; + } + + scrubTo(mFrame + frameSpacing); + + backup(tr("Import Image")); + + progressChanged(qFloor(qMin(static_cast(reader.currentImageNumber()) / totalFrames, 1.0) * 100)); + } + + return Status::OK; } void Editor::selectAll() const diff --git a/core_lib/src/interface/editor.h b/core_lib/src/interface/editor.h index eef05b266..a3ba0e179 100644 --- a/core_lib/src/interface/editor.h +++ b/core_lib/src/interface/editor.h @@ -174,7 +174,7 @@ class Editor : public QObject void clearCurrentFrame(); Status importImage(const QString& filePath); - Status importGIF(const QString& filePath, int numOfImages = 0); + Status importAnimatedImage(const QString& filePath, int frameSpacing, const std::function& progressChanged, const std::function& wasCanceled); void restoreKey(); void scrubNextKeyFrame(); @@ -230,7 +230,7 @@ class Editor : public QObject void resetAutoSaveCounter(); private: - Status importBitmapImage(const QString&, int space = 0); + Status importBitmapImage(const QString&); Status importVectorImage(const QString&); void pasteToCanvas(BitmapImage* bitmapImage, int frameNumber); diff --git a/core_lib/src/structure/object.cpp b/core_lib/src/structure/object.cpp index 17834e6f7..bb25cdb1e 100644 --- a/core_lib/src/structure/object.cpp +++ b/core_lib/src/structure/object.cpp @@ -828,6 +828,10 @@ bool Object::exportFrames(int frameStart, int frameEnd, extension = ".bmp"; transparency = false; } + if (formatStr == "WEBP" || formatStr == "webp") { + format = "WEBP"; + extension = ".webp"; + } if (filePath.endsWith(extension, Qt::CaseInsensitive)) { filePath.chop(extension.size()); diff --git a/core_lib/src/util/fileformat.cpp b/core_lib/src/util/fileformat.cpp index 55707cfa5..3d4eb7707 100644 --- a/core_lib/src/util/fileformat.cpp +++ b/core_lib/src/util/fileformat.cpp @@ -59,6 +59,7 @@ QString detectFormatByFileNameExtension(const QString& fileName) { "tif", "TIF" }, { "tiff", "TIF" }, { "bmp", "BMP" }, + { "webp", "WEBP" }, { "mp4", "MP4" }, { "avi", "AVI" }, { "gif", "GIF" }, diff --git a/core_lib/src/util/fileformat.h b/core_lib/src/util/fileformat.h index 2ce07e5fa..6935d0aa4 100644 --- a/core_lib/src/util/fileformat.h +++ b/core_lib/src/util/fileformat.h @@ -40,10 +40,10 @@ GNU General Public License for more details. ";;SWF(*.swf);;FLV(*.flv);;WEBM(*.webm);;WMV(*.wmv)" #define PFF_IMAGE_FILTER \ - QCoreApplication::translate("FileFormat", "Image formats") + " (*.png *.jpg *.jpeg *.bmp *.tif *.tiff);;PNG (*.png);;JPG(*.jpg *.jpeg);;BMP(*.bmp);;TIFF(*.tif *.tiff)" + QCoreApplication::translate("FileFormat", "Image formats") + " (*.png *.jpg *.jpeg *.bmp *.tif *.tiff *.webp);;PNG (*.png);;JPG(*.jpg *.jpeg);;BMP(*.bmp);;TIFF(*.tif *.tiff);;WEBP(*.webp)" #define PFF_IMAGE_SEQ_FILTER \ - QCoreApplication::translate("FileFormat", "Image formats") + " (*.png *.jpg *.jpeg *.bmp *.tif *.tiff);;PNG (*.png);;JPG(*.jpg *.jpeg);;BMP(*.bmp);;TIFF(*.tif *.tiff)" + QCoreApplication::translate("FileFormat", "Image formats") + " (*.png *.jpg *.jpeg *.bmp *.tif *.tiff *.webp);;PNG (*.png);;JPG(*.jpg *.jpeg);;BMP(*.bmp);;TIFF(*.tif *.tiff);;WEBP(*.webp)" #define PFF_PALETTE_EXT_FILTER \ QCoreApplication::translate("FileFormat", "Palette formats") + " (*.xml *.gpl);;" + QCoreApplication::translate("FileFormat", "Pencil2D Palette") + " (*.xml);;" + QCoreApplication::translate("FileFormat", "GIMP Palette") + " (*.gpl)" @@ -51,6 +51,9 @@ GNU General Public License for more details. #define PFF_GIF_EXT_FILTER \ QCoreApplication::translate("FileFormat", "Animated GIF") + " (*.gif)" +#define PFF_ANIMATED_IMAGE_EXT_FILTER \ + QCoreApplication::translate("FileFormat", "Animated image formats") + " (*.gif *.webp);;GIF(*.gif);;WEBP(*.webp)" + #define PFF_SOUND_EXT_FILTER \ QCoreApplication::translate("FileFormat", "Sound formats") + " (*.wav *.mp3 *.wma *.ogg *.flac *.opus *.aiff *.aac *.caf);;WAV (*.wav);;MP3 (*.mp3);;WMA (*.wma);;OGG (*.ogg);;FLAC (*.flac);;Opus (*.opus);;AIFF (*.aiff);;AAC (*.aac);;CAF (*.caf)" diff --git a/core_lib/src/util/filetype.h b/core_lib/src/util/filetype.h index a0558fa5a..9a88fad1e 100644 --- a/core_lib/src/util/filetype.h +++ b/core_lib/src/util/filetype.h @@ -7,6 +7,7 @@ enum class FileType IMAGE, IMAGE_SEQUENCE, GIF, + ANIMATED_IMAGE, MOVIE, SOUND, PALETTE