From d160fb8b913810a2bbf00baaf03254c27bfdd0ce Mon Sep 17 00:00:00 2001 From: David Capello Date: Mon, 13 Jun 2022 12:46:11 -0300 Subject: [PATCH 1/9] Minor changes in SaveFileCommand to use enums instead of bool params --- src/app/commands/cmd_save_file.cpp | 28 +++++++++++++++++----------- src/app/commands/cmd_save_file.h | 10 +++++++--- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/app/commands/cmd_save_file.cpp b/src/app/commands/cmd_save_file.cpp index 2253a55b0..335e80f35 100644 --- a/src/app/commands/cmd_save_file.cpp +++ b/src/app/commands/cmd_save_file.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2020 Igara Studio S.A. +// Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -129,8 +129,8 @@ std::string SaveFileBaseCommand::saveAsDialog( Context* context, const std::string& dlgTitle, const std::string& initialFilename, - const bool markAsSaved, - const bool saveInBackground, + const MarkAsSaved markAsSaved, + const SaveInBackground saveInBackground, const std::string& forbiddenFilename) { Doc* document = context->activeDocument(); @@ -170,7 +170,7 @@ std::string SaveFileBaseCommand::saveAsDialog( #endif // ENABLE_UI } - if (saveInBackground) { + if (saveInBackground == SaveInBackground::On) { saveDocumentInBackground( context, document, filename, markAsSaved); @@ -198,7 +198,7 @@ void SaveFileBaseCommand::saveDocumentInBackground( const Context* context, Doc* document, const std::string& filename, - const bool markAsSaved) + const MarkAsSaved markAsSaved) { if (!m_aniDir.empty()) { switch (convert_string_to_anidir(m_aniDir)) { @@ -241,7 +241,7 @@ void SaveFileBaseCommand::saveDocumentInBackground( } else if (context->isUIAvailable()) { App::instance()->recentFiles()->addRecentFile(filename); - if (markAsSaved) { + if (markAsSaved == MarkAsSaved::On) { document->markAsSaved(); document->setFilename(filename); document->incrementVersion(); @@ -283,14 +283,16 @@ void SaveFileCommand::onExecute(Context* context) saveDocumentInBackground( context, document, - documentReader->filename(), true); + documentReader->filename(), + MarkAsSaved::On); } // If the document isn't associated to a file, we must to show the // save-as dialog to the user to select for first time the file-name // for this document. else { saveAsDialog(context, "Save File", - document->filename(), true); + document->filename(), + MarkAsSaved::On); } } @@ -311,7 +313,8 @@ void SaveFileAsCommand::onExecute(Context* context) { Doc* document = context->activeDocument(); saveAsDialog(context, "Save As", - document->filename(), true); + document->filename(), + MarkAsSaved::On); } class SaveFileCopyAsCommand : public SaveFileBaseCommand { @@ -353,7 +356,9 @@ void SaveFileCopyAsCommand::onExecute(Context* context) std::string result = saveAsDialog( context, "Export", - win.outputFilenameValue(), false, false, + win.outputFilenameValue(), + MarkAsSaved::Off, + SaveInBackground::Off, (doc->isAssociatedToFile() ? doc->filename(): std::string())); if (!result.empty()) @@ -461,7 +466,8 @@ void SaveFileCopyAsCommand::onExecute(Context* context) PngEncoderOneAlphaPixel fixPng(isForTwitter); saveDocumentInBackground( - context, doc, outputFilename, false); + context, doc, outputFilename, + MarkAsSaved::Off); } // Undo resize diff --git a/src/app/commands/cmd_save_file.h b/src/app/commands/cmd_save_file.h index 348eaacdb..635900a5f 100644 --- a/src/app/commands/cmd_save_file.h +++ b/src/app/commands/cmd_save_file.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -18,6 +19,9 @@ namespace app { class SaveFileBaseCommand : public Command { public: + enum class MarkAsSaved { Off, On }; + enum class SaveInBackground { Off, On }; + SaveFileBaseCommand(const char* id, CommandFlags flags); protected: @@ -28,14 +32,14 @@ namespace app { Context* context, const std::string& dlgTitle, const std::string& filename, - const bool markAsSaved, - const bool saveInBackground = true, + const MarkAsSaved markAsSaved, + const SaveInBackground saveInBackground = SaveInBackground::On, const std::string& forbiddenFilename = std::string()); void saveDocumentInBackground( const Context* context, Doc* document, const std::string& filename, - const bool markAsSaved); + const MarkAsSaved markAsSaved); std::string m_filename; std::string m_filenameFormat; From ddc1b762144330f15ffbe0ce8388365fc9a1e476 Mon Sep 17 00:00:00 2001 From: David Capello Date: Mon, 13 Jun 2022 17:04:12 -0300 Subject: [PATCH 2/9] Convert SaveFileBaseCommand to CommandWithNewParams With this patch we fixed some use cases: * We can show the ui with a default filename { filename=..., ui=true } * We can specify fromFrame/toFrame for SaveFileCopyAs --- src/app/commands/cmd_save_file.cpp | 84 ++++++++++++++---------------- src/app/commands/cmd_save_file.h | 23 +++++--- src/app/commands/new_params.cpp | 18 ++++++- src/app/script/api_version.h | 2 +- src/app/ui/export_file_window.cpp | 7 ++- src/app/ui/export_file_window.h | 4 +- 6 files changed, 80 insertions(+), 58 deletions(-) diff --git a/src/app/commands/cmd_save_file.cpp b/src/app/commands/cmd_save_file.cpp index 335e80f35..f4f579d48 100644 --- a/src/app/commands/cmd_save_file.cpp +++ b/src/app/commands/cmd_save_file.cpp @@ -86,24 +86,18 @@ private: ////////////////////////////////////////////////////////////////////// SaveFileBaseCommand::SaveFileBaseCommand(const char* id, CommandFlags flags) - : Command(id, flags) + : CommandWithNewParams(id, flags) { - m_useUI = true; - m_ignoreEmpty = false; } void SaveFileBaseCommand::onLoadParams(const Params& params) { - m_filename = params.get("filename"); - m_filenameFormat = params.get("filename-format"); - m_tag = params.get("frame-tag"); - m_aniDir = params.get("ani-dir"); - m_slice = params.get("slice"); + CommandWithNewParams::onLoadParams(params); - if (params.has_param("from-frame") || - params.has_param("to-frame")) { - doc::frame_t fromFrame = params.get_as("from-frame"); - doc::frame_t toFrame = params.get_as("to-frame"); + if (this->params().fromFrame.isSet() || + this->params().toFrame.isSet()) { + doc::frame_t fromFrame = this->params().fromFrame(); + doc::frame_t toFrame = this->params().toFrame(); m_selFrames.insert(fromFrame, toFrame); m_adjustFramesByTag = true; } @@ -111,11 +105,6 @@ void SaveFileBaseCommand::onLoadParams(const Params& params) m_selFrames.clear(); m_adjustFramesByTag = false; } - - std::string useUI = params.get("useUI"); - m_useUI = (useUI.empty() || (useUI == "true")); - - m_ignoreEmpty = params.get_as("ignoreEmpty"); } // Returns true if there is a current sprite to save. @@ -141,19 +130,15 @@ std::string SaveFileBaseCommand::saveAsDialog( // preferences. Preferences::instance().save(); - std::string filename; - - if (!m_filename.empty()) { - filename = m_filename; - } - else { + std::string filename = params().filename(); + if (filename.empty() || params().ui()) { base::paths exts = get_writable_extensions(); filename = initialFilename; #ifdef ENABLE_UI again:; base::paths newfilename; - if (!m_useUI || + if (!params().ui() || !app::show_file_selector( dlgTitle, filename, exts, FileSelectorType::Save, @@ -200,8 +185,8 @@ void SaveFileBaseCommand::saveDocumentInBackground( const std::string& filename, const MarkAsSaved markAsSaved) { - if (!m_aniDir.empty()) { - switch (convert_string_to_anidir(m_aniDir)) { + if (params().aniDir.isSet()) { + switch (params().aniDir()) { case AniDir::REVERSE: m_selFrames = m_selFrames.makeReverse(); break; @@ -211,7 +196,7 @@ void SaveFileBaseCommand::saveDocumentInBackground( } } - FileOpROI roi(document, m_slice, m_tag, + FileOpROI roi(document, params().slice(), params().tag(), m_selFrames, m_adjustFramesByTag); std::unique_ptr fop( @@ -219,8 +204,8 @@ void SaveFileBaseCommand::saveDocumentInBackground( context, roi, filename, - m_filenameFormat, - m_ignoreEmpty)); + params().filenameFormat(), + params().ignoreEmpty())); if (!fop) return; @@ -283,7 +268,8 @@ void SaveFileCommand::onExecute(Context* context) saveDocumentInBackground( context, document, - documentReader->filename(), + (params().filename.isSet() ? params().filename(): + documentReader->filename()), MarkAsSaved::On); } // If the document isn't associated to a file, we must to show the @@ -291,7 +277,8 @@ void SaveFileCommand::onExecute(Context* context) // for this document. else { saveAsDialog(context, "Save File", - document->filename(), + (params().filename.isSet() ? params().filename(): + document->filename()), MarkAsSaved::On); } } @@ -313,7 +300,8 @@ void SaveFileAsCommand::onExecute(Context* context) { Doc* document = context->activeDocument(); saveAsDialog(context, "Save As", - document->filename(), + (params().filename.isSet() ? params().filename(): + document->filename()), MarkAsSaved::On); } @@ -337,17 +325,17 @@ SaveFileCopyAsCommand::SaveFileCopyAsCommand() void SaveFileCopyAsCommand::onExecute(Context* context) { Doc* doc = context->activeDocument(); - std::string outputFilename = m_filename; + std::string outputFilename = params().filename(); std::string layers = kAllLayers; std::string frames = kAllFrames; double xscale = 1.0; double yscale = 1.0; bool applyPixelRatio = false; - doc::AniDir aniDirValue = convert_string_to_anidir(m_aniDir); + doc::AniDir aniDirValue = params().aniDir(); bool isForTwitter = false; #if ENABLE_UI - if (m_useUI && context->isUIAvailable()) { + if (params().ui() && context->isUIAvailable()) { ExportFileWindow win(doc); bool askOverwrite = true; @@ -367,6 +355,8 @@ void SaveFileCopyAsCommand::onExecute(Context* context) return result; }); + win.setAniDir(aniDirValue); + win.remapWindow(); load_window_pos(&win, "ExportFile"); again:; @@ -446,20 +436,22 @@ void SaveFileCopyAsCommand::onExecute(Context* context) layers, layersVisibility); - // Selected frames to export - SelectedFrames selFrames; - Tag* tag = calculate_selected_frames( - site, frames, selFrames); - if (tag) - m_tag = tag->name(); - m_selFrames = selFrames; + // m_selFrames is not empty if fromFrame/toFrame parameters are + // specified. + if (m_selFrames.empty()) { + // Selected frames to export + SelectedFrames selFrames; + Tag* tag = calculate_selected_frames( + site, frames, selFrames); + if (tag) + params().tag(tag->name()); + m_selFrames = selFrames; + } m_adjustFramesByTag = false; } - base::ScopedValue restoreAniDir( - m_aniDir, - convert_anidir_to_string(aniDirValue), // New value - m_aniDir); // Restore old value + // Set ani dir + params().aniDir(aniDirValue); // TODO This should be set as options for the specific encoder GifEncoderDurationFix fixGif(isForTwitter); diff --git a/src/app/commands/cmd_save_file.h b/src/app/commands/cmd_save_file.h index 635900a5f..f165a1046 100644 --- a/src/app/commands/cmd_save_file.h +++ b/src/app/commands/cmd_save_file.h @@ -10,6 +10,8 @@ #pragma once #include "app/commands/command.h" +#include "app/commands/new_params.h" +#include "doc/anidir.h" #include "doc/selected_frames.h" #include @@ -17,7 +19,19 @@ namespace app { class Doc; - class SaveFileBaseCommand : public Command { + struct SaveFileParams : public NewParams { + Param ui { this, true, { "ui", "useUI" } }; + Param filename { this, std::string(), "filename" }; + Param filenameFormat { this, std::string(), { "filenameFormat", "filename-format" } }; + Param tag { this, std::string(), { "tag", "frame-tag" } }; + Param aniDir { this, doc::AniDir::FORWARD, { "aniDir", "ani-dir" } }; + Param slice { this, std::string(), "slice" }; + Param fromFrame { this, 0, { "fromFrame", "from-frame" } }; + Param toFrame { this, 0, { "toFrame", "to-frame" } }; + Param ignoreEmpty { this, false, "ignoreEmpty" }; + }; + + class SaveFileBaseCommand : public CommandWithNewParams { public: enum class MarkAsSaved { Off, On }; enum class SaveInBackground { Off, On }; @@ -41,15 +55,8 @@ namespace app { const std::string& filename, const MarkAsSaved markAsSaved); - std::string m_filename; - std::string m_filenameFormat; - std::string m_tag; - std::string m_aniDir; - std::string m_slice; doc::SelectedFrames m_selFrames; bool m_adjustFramesByTag; - bool m_useUI; - bool m_ignoreEmpty; }; } // namespace app diff --git a/src/app/commands/new_params.cpp b/src/app/commands/new_params.cpp index 0dd0f6309..f3f10ea75 100644 --- a/src/app/commands/new_params.cpp +++ b/src/app/commands/new_params.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2021 Igara Studio S.A. +// Copyright (C) 2019-2022 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -18,6 +18,7 @@ #include "base/split_string.h" #include "base/string.h" #include "doc/algorithm/resize_image.h" +#include "doc/anidir.h" #include "doc/color_mode.h" #include "filters/color_curve.h" #include "filters/hue_saturation_filter.h" @@ -144,6 +145,12 @@ void Param::fromString(const std::string& value) setValue(doc::ColorMode::RGB); } +template<> +void Param::fromString(const std::string& value) +{ + setValue(convert_string_to_anidir(value)); +} + template<> void Param::fromString(const std::string& value) { @@ -303,6 +310,15 @@ void Param::fromLua(lua_State* L, int index) setValue((doc::ColorMode)lua_tointeger(L, index)); } +template<> +void Param::fromLua(lua_State* L, int index) +{ + if (lua_type(L, index) == LUA_TSTRING) + fromString(lua_tostring(L, index)); + else + setValue((doc::AniDir)lua_tointeger(L, index)); +} + template<> void Param::fromLua(lua_State* L, int index) { diff --git a/src/app/script/api_version.h b/src/app/script/api_version.h index 4f66ee469..c5269d02f 100644 --- a/src/app/script/api_version.h +++ b/src/app/script/api_version.h @@ -10,6 +10,6 @@ // Increment this value if the scripting API is modified between two // released Aseprite versions. -#define API_VERSION 18 +#define API_VERSION 19 #endif diff --git a/src/app/ui/export_file_window.cpp b/src/app/ui/export_file_window.cpp index 476279d7e..f84de4983 100644 --- a/src/app/ui/export_file_window.cpp +++ b/src/app/ui/export_file_window.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2020 Igara Studio S.A. +// Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -141,6 +141,11 @@ bool ExportFileWindow::isForTwitter() const return forTwitter()->isSelected(); } +void ExportFileWindow::setAniDir(const doc::AniDir aniDir) +{ + anidir()->setSelectedItemIndex(int(aniDir)); +} + void ExportFileWindow::setOutputFilename(const std::string& pathAndFilename) { m_outputPath = base::get_file_path(pathAndFilename); diff --git a/src/app/ui/export_file_window.h b/src/app/ui/export_file_window.h index 9aa1338a2..80b8c29a1 100644 --- a/src/app/ui/export_file_window.h +++ b/src/app/ui/export_file_window.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2020 Igara Studio S.A. +// Copyright (C) 2020-2022 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -34,6 +34,8 @@ namespace app { bool applyPixelRatio() const; bool isForTwitter() const; + void setAniDir(const doc::AniDir aniDir); + obs::signal SelectOutputFile; private: From 82cd244a6e43c299e902798455330b0677887e9f Mon Sep 17 00:00:00 2001 From: David Capello Date: Mon, 13 Jun 2022 19:19:44 -0300 Subject: [PATCH 3/9] Add support to use SaveFile commands from CLI (without UI) --- src/app/commands/cmd_save_file.cpp | 49 ++++++++++++++++++------------ src/app/pref/preferences.cpp | 7 +++-- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/app/commands/cmd_save_file.cpp b/src/app/commands/cmd_save_file.cpp index f4f579d48..45c8f4ff1 100644 --- a/src/app/commands/cmd_save_file.cpp +++ b/src/app/commands/cmd_save_file.cpp @@ -136,25 +136,31 @@ std::string SaveFileBaseCommand::saveAsDialog( filename = initialFilename; #ifdef ENABLE_UI - again:; - base::paths newfilename; - if (!params().ui() || - !app::show_file_selector( - dlgTitle, filename, exts, - FileSelectorType::Save, - newfilename)) - return std::string(); + if (context->isUIAvailable()) { + again:; + base::paths newfilename; + if (!params().ui() || + !app::show_file_selector( + dlgTitle, filename, exts, + FileSelectorType::Save, + newfilename)) { + return std::string(); + } - filename = newfilename.front(); - if (!forbiddenFilename.empty() && - base::normalize_path(forbiddenFilename) == - base::normalize_path(filename)) { - ui::Alert::show(Strings::alerts_cannot_file_overwrite_on_export()); - goto again; + filename = newfilename.front(); + if (!forbiddenFilename.empty() && + base::normalize_path(forbiddenFilename) == + base::normalize_path(filename)) { + ui::Alert::show(Strings::alerts_cannot_file_overwrite_on_export()); + goto again; + } } #endif // ENABLE_UI } + if (filename.empty()) + return std::string(); + if (saveInBackground == SaveInBackground::On) { saveDocumentInBackground( context, document, @@ -224,17 +230,22 @@ void SaveFileBaseCommand::saveDocumentInBackground( else if (fop->isStop()) { document->impossibleToBackToSavedState(); } - else if (context->isUIAvailable()) { - App::instance()->recentFiles()->addRecentFile(filename); + else { + if (context->isUIAvailable() && params().ui()) + App::instance()->recentFiles()->addRecentFile(filename); + if (markAsSaved == MarkAsSaved::On) { document->markAsSaved(); document->setFilename(filename); document->incrementVersion(); } + #ifdef ENABLE_UI - StatusBar::instance()->setStatusText( - 2000, fmt::format("File <{}> saved.", - base::get_file_name(filename))); + if (context->isUIAvailable() && params().ui()) { + StatusBar::instance()->setStatusText( + 2000, fmt::format("File <{}> saved.", + base::get_file_name(filename))); + } #endif } } diff --git a/src/app/pref/preferences.cpp b/src/app/pref/preferences.cpp index dbb278504..fa64eacbf 100644 --- a/src/app/pref/preferences.cpp +++ b/src/app/pref/preferences.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2021 Igara Studio S.A. +// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -104,7 +104,10 @@ void Preferences::load() void Preferences::save() { - ui::assert_ui_thread(); +#ifdef _DEBUG + if (ui::UISystem::instance()) + ui::assert_ui_thread(); +#endif app::gen::GlobalPref::save(); for (auto& pair : m_tools) From 30b61e501e73be59e0850fd9e3b8516af01a4dc4 Mon Sep 17 00:00:00 2001 From: David Capello Date: Tue, 14 Jun 2022 10:04:54 -0300 Subject: [PATCH 4/9] [lua] New Image(Image, Rectangle) ctor --- src/app/script/image_class.cpp | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/app/script/image_class.cpp b/src/app/script/image_class.cpp index 1b3becacc..eb811de5d 100644 --- a/src/app/script/image_class.cpp +++ b/src/app/script/image_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2021 Igara Studio S.A. +// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2015-2018 David Capello // // This program is distributed under the terms of @@ -100,8 +100,29 @@ int Image_new(lua_State* L) if (auto spec2 = may_get_obj(L, 1)) { spec = *spec2; } - else if (may_get_obj(L, 1)) { - return Image_clone(L); + else if (auto imgObj = may_get_obj(L, 1)) { + // Copy a region of the image + if (auto rc = may_get_obj(L, 2)) { + doc::Image* crop = nullptr; + try { + auto docImg = imgObj->image(L); + crop = doc::crop_image(docImg, *rc, docImg->maskColor()); + } + catch (const std::invalid_argument&) { + // Do nothing (will return nil) + } + if (crop) { + push_new(L, crop); + return 1; + } + else { + return 0; + } + } + // Copy the whole image + else { + return Image_clone(L); + } } else if (auto spr = may_get_docobj(L, 1)) { image = doc::Image::create(spr->spec()); From e2798bc849b86400d991ac917b11c25f5c1d0203 Mon Sep 17 00:00:00 2001 From: David Capello Date: Tue, 14 Jun 2022 14:00:19 -0300 Subject: [PATCH 5/9] Fix crash creating the "No Recent File" w/AppMenuItem() (fix #3371) --- src/app/app_menus.cpp | 2 +- src/app/ui/app_menuitem.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/app_menus.cpp b/src/app/app_menus.cpp index b5ece5f96..47be5621f 100644 --- a/src/app/app_menus.cpp +++ b/src/app/app_menus.cpp @@ -546,7 +546,7 @@ bool AppMenus::rebuildRecentList() else { std::unique_ptr menuitem( new AppMenuItem( - Strings::main_menu_file_no_recent_file(), nullptr)); + Strings::main_menu_file_no_recent_file())); menuitem->setIsRecentFileItem(true); menuitem->setEnabled(false); diff --git a/src/app/ui/app_menuitem.h b/src/app/ui/app_menuitem.h index 0d6d3e98c..0edbfced2 100644 --- a/src/app/ui/app_menuitem.h +++ b/src/app/ui/app_menuitem.h @@ -35,6 +35,7 @@ namespace app { AppMenuItem(const std::string& text, const std::string& commandId = std::string(), const Params& params = Params()); + AppMenuItem(const std::string& text, std::nullptr_t) = delete; KeyPtr key() { return m_key; } void setKey(const KeyPtr& key); From c58dae51fa9548704c4255ac198a71eb371498b3 Mon Sep 17 00:00:00 2001 From: David Capello Date: Tue, 14 Jun 2022 21:38:47 -0300 Subject: [PATCH 6/9] Fix crash using a deleted surface in Overlay::restoreOverlappedArea() Can happen when a resize event is received and m_captured stores the surface with the old window size. This was found opening a file from the CLI on Linux/X11. --- src/ui/overlay.cpp | 6 +++--- src/ui/overlay.h | 6 +++--- src/ui/overlay_manager.cpp | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ui/overlay.cpp b/src/ui/overlay.cpp index 729057304..0ec5a02dd 100644 --- a/src/ui/overlay.cpp +++ b/src/ui/overlay.cpp @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2018 Igara Studio S.A. +// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2001-2016 David Capello // // This file is released under the terms of the MIT license. @@ -83,7 +83,7 @@ void Overlay::moveOverlay(const gfx::Point& newPos) m_pos = newPos; } -void Overlay::captureOverlappedArea(os::Surface* screen) +void Overlay::captureOverlappedArea(const os::SurfaceRef& screen) { if (!m_surface || m_captured) @@ -116,7 +116,7 @@ void Overlay::restoreOverlappedArea(const gfx::Rect& restoreBounds) return; os::SurfaceLock lock(m_overlap.get()); - m_overlap->blitTo(m_captured, 0, 0, m_pos.x, m_pos.y, + m_overlap->blitTo(m_captured.get(), 0, 0, m_pos.x, m_pos.y, m_overlap->width(), m_overlap->height()); Manager::getDefault()->dirtyRect(bounds()); diff --git a/src/ui/overlay.h b/src/ui/overlay.h index 79ed040eb..7da75d56b 100644 --- a/src/ui/overlay.h +++ b/src/ui/overlay.h @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2018-2020 Igara Studio S.A. +// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2001-2016 David Capello // // This file is released under the terms of the MIT license. @@ -41,7 +41,7 @@ namespace ui { const gfx::Point& position() const { return m_pos; } gfx::Rect bounds() const; - void captureOverlappedArea(os::Surface* screen); + void captureOverlappedArea(const os::SurfaceRef& screen); void restoreOverlappedArea(const gfx::Rect& restoreBounds); void drawOverlay(); @@ -57,7 +57,7 @@ namespace ui { // Surface where we captured the overlapped (m_overlap) // region. It's nullptr if the overlay wasn't drawn yet. - os::Surface* m_captured; + os::SurfaceRef m_captured; gfx::Point m_pos; ZOrder m_zorder; diff --git a/src/ui/overlay_manager.cpp b/src/ui/overlay_manager.cpp index 597902c68..3356ed2cd 100644 --- a/src/ui/overlay_manager.cpp +++ b/src/ui/overlay_manager.cpp @@ -1,5 +1,5 @@ // Aseprite UI Library -// Copyright (C) 2018-2021 Igara Studio S.A. +// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2001-2016 David Capello // // This file is released under the terms of the MIT license. @@ -86,8 +86,8 @@ void OverlayManager::drawOverlays() if (!manager) return; - os::Surface* displaySurface = manager->display()->surface(); - os::SurfaceLock lock(displaySurface); + os::SurfaceRef displaySurface(base::AddRef(manager->display()->surface())); + os::SurfaceLock lock(displaySurface.get()); for (auto& overlay : *this) overlay->captureOverlappedArea(displaySurface); From 4aa5fedfec2b809619763eac68b95fca1e46f20c Mon Sep 17 00:00:00 2001 From: David Capello Date: Tue, 14 Jun 2022 23:19:39 -0300 Subject: [PATCH 7/9] New "on the fly" resize when saving/exporting image (fix #1112) Implemented using a new FileAbstractImage interface to get scanlines for each frame automatically resized (without modifying the original sprite/without using SpriteSize command/adding new undo information). Related to #3008 --- src/app/commands/cmd_save_file.cpp | 43 ++++++-- src/app/commands/cmd_save_file.h | 9 +- src/app/file/bmp_format.cpp | 60 +++++----- src/app/file/css_format.cpp | 8 +- src/app/file/file.cpp | 170 ++++++++++++++++++++++++++++- src/app/file/file.h | 45 +++++++- src/app/file/file_format.h | 1 + src/app/file/gif_format.cpp | 34 +++--- src/app/file/jpeg_format.cpp | 24 ++-- src/app/file/pcx_format.cpp | 62 ++++++----- src/app/file/png_format.cpp | 51 +++++---- src/app/file/svg_format.cpp | 8 +- src/app/file/tga_format.cpp | 26 +++-- src/app/ui/export_file_window.cpp | 6 + src/app/ui/export_file_window.h | 3 +- 15 files changed, 403 insertions(+), 147 deletions(-) diff --git a/src/app/commands/cmd_save_file.cpp b/src/app/commands/cmd_save_file.cpp index 45c8f4ff1..f6653648a 100644 --- a/src/app/commands/cmd_save_file.cpp +++ b/src/app/commands/cmd_save_file.cpp @@ -189,7 +189,9 @@ void SaveFileBaseCommand::saveDocumentInBackground( const Context* context, Doc* document, const std::string& filename, - const MarkAsSaved markAsSaved) + const MarkAsSaved markAsSaved, + const ResizeOnTheFly resizeOnTheFly, + const gfx::PointF& scale) { if (params().aniDir.isSet()) { switch (params().aniDir()) { @@ -215,6 +217,9 @@ void SaveFileBaseCommand::saveDocumentInBackground( if (!fop) return; + if (resizeOnTheFly == ResizeOnTheFly::On) + fop->setOnTheFlyScale(scale); + SaveFileJob job(fop.get()); job.showProgressWindow(); @@ -339,9 +344,8 @@ void SaveFileCopyAsCommand::onExecute(Context* context) std::string outputFilename = params().filename(); std::string layers = kAllLayers; std::string frames = kAllFrames; - double xscale = 1.0; - double yscale = 1.0; bool applyPixelRatio = false; + gfx::PointF scale(params().scale(), params().scale()); doc::AniDir aniDirValue = params().aniDir(); bool isForTwitter = false; @@ -366,7 +370,17 @@ void SaveFileCopyAsCommand::onExecute(Context* context) return result; }); - win.setAniDir(aniDirValue); + if (params().filename.isSet()) { + std::string outputPath = base::get_file_path(outputFilename); + if (outputPath.empty()) { + outputPath = base::get_file_path(doc->filename()); + outputFilename = base::join_path(outputPath, outputFilename); + } + win.setOutputFilename(outputFilename); + } + + if (params().scale.isSet()) win.setResizeScale(scale); + if (params().aniDir.isSet()) win.setAniDir(aniDirValue); win.remapWindow(); load_window_pos(&win, "ExportFile"); @@ -395,7 +409,7 @@ void SaveFileCopyAsCommand::onExecute(Context* context) layers = win.layersValue(); frames = win.framesValue(); - xscale = yscale = win.resizeValue(); + scale.x = scale.y = win.resizeValue(); applyPixelRatio = win.applyPixelRatio(); aniDirValue = win.aniDirValue(); isForTwitter = win.isForTwitter(); @@ -405,21 +419,23 @@ void SaveFileCopyAsCommand::onExecute(Context* context) // Pixel ratio if (applyPixelRatio) { doc::PixelRatio pr = doc->sprite()->pixelRatio(); - xscale *= pr.w; - yscale *= pr.h; + scale.x *= pr.w; + scale.y *= pr.h; } - // Apply scale + // First of all we'll try to use the "on the fly" scaling, to avoid + // using a resize command to apply the export scale. const undo::UndoState* undoState = nullptr; bool undoResize = false; - if (xscale != 1.0 || yscale != 1.0) { + const bool resizeOnTheFly = FileOp::checkIfFormatSupportResizeOnTheFly(outputFilename); + if (!resizeOnTheFly && (scale.x != 1.0 || scale.y != 1.0)) { Command* resizeCmd = Commands::instance()->byId(CommandId::SpriteSize()); ASSERT(resizeCmd); if (resizeCmd) { int width = doc->sprite()->width(); int height = doc->sprite()->height(); - int newWidth = int(double(width) * xscale); - int newHeight = int(double(height) * yscale); + int newWidth = int(double(width) * scale.x); + int newHeight = int(double(height) * scale.y); if (newWidth < 1) newWidth = 1; if (newHeight < 1) newHeight = 1; if (width != newWidth || height != newHeight) { @@ -470,7 +486,10 @@ void SaveFileCopyAsCommand::onExecute(Context* context) saveDocumentInBackground( context, doc, outputFilename, - MarkAsSaved::Off); + MarkAsSaved::Off, + (resizeOnTheFly ? ResizeOnTheFly::On: + ResizeOnTheFly::Off), + scale); } // Undo resize diff --git a/src/app/commands/cmd_save_file.h b/src/app/commands/cmd_save_file.h index f165a1046..0de06d712 100644 --- a/src/app/commands/cmd_save_file.h +++ b/src/app/commands/cmd_save_file.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2022 Igara Studio S.A. +// Copyright (C) 2021-2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -13,6 +13,7 @@ #include "app/commands/new_params.h" #include "doc/anidir.h" #include "doc/selected_frames.h" +#include "gfx/point.h" #include @@ -29,12 +30,14 @@ namespace app { Param fromFrame { this, 0, { "fromFrame", "from-frame" } }; Param toFrame { this, 0, { "toFrame", "to-frame" } }; Param ignoreEmpty { this, false, "ignoreEmpty" }; + Param scale { this, 1.0, "scale" }; }; class SaveFileBaseCommand : public CommandWithNewParams { public: enum class MarkAsSaved { Off, On }; enum class SaveInBackground { Off, On }; + enum class ResizeOnTheFly { Off, On }; SaveFileBaseCommand(const char* id, CommandFlags flags); @@ -53,7 +56,9 @@ namespace app { const Context* context, Doc* document, const std::string& filename, - const MarkAsSaved markAsSaved); + const MarkAsSaved markAsSaved, + const ResizeOnTheFly resizeOnTheFly = ResizeOnTheFly::Off, + const gfx::PointF& scale = gfx::PointF(1.0, 1.0)); doc::SelectedFrames m_selFrames; bool m_adjustFramesByTag; diff --git a/src/app/file/bmp_format.cpp b/src/app/file/bmp_format.cpp index 94746c81a..b2e34c43e 100644 --- a/src/app/file/bmp_format.cpp +++ b/src/app/file/bmp_format.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2020 Igara Studio S.A. +// Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -67,7 +67,8 @@ class BmpFormat : public FileFormat { FILE_SUPPORT_RGB | FILE_SUPPORT_GRAY | FILE_SUPPORT_INDEXED | - FILE_SUPPORT_SEQUENCES; + FILE_SUPPORT_SEQUENCES | + FILE_ENCODE_ABSTRACT_IMAGE; } bool onLoad(FileOp* fop) override; @@ -688,34 +689,34 @@ bool BmpFormat::onLoad(FileOp *fop) else rmask = gmask = bmask = 0; - Image* image = fop->sequenceImage(pixelFormat, - infoheader.biWidth, - ABS((int)infoheader.biHeight)); + ImageRef image = fop->sequenceImage(pixelFormat, + infoheader.biWidth, + ABS((int)infoheader.biHeight)); if (!image) { return false; } if (pixelFormat == IMAGE_RGB) - clear_image(image, rgba(0, 0, 0, 255)); + clear_image(image.get(), rgba(0, 0, 0, 255)); else - clear_image(image, 0); + clear_image(image.get(), 0); switch (infoheader.biCompression) { case BI_RGB: - read_image(f, image, &infoheader, fop); + read_image(f, image.get(), &infoheader, fop); break; case BI_RLE8: - read_rle8_compressed_image(f, image, &infoheader); + read_rle8_compressed_image(f, image.get(), &infoheader); break; case BI_RLE4: - read_rle4_compressed_image(f, image, &infoheader); + read_rle4_compressed_image(f, image.get(), &infoheader); break; case BI_BITFIELDS: - if (read_bitfields_image(f, image, &infoheader, rmask, gmask, bmask) < 0) { + if (read_bitfields_image(f, image.get(), &infoheader, rmask, gmask, bmask) < 0) { fop->setError("Unsupported bitfields in the BMP file.\n"); return false; } @@ -751,21 +752,22 @@ bool BmpFormat::onLoad(FileOp *fop) #ifdef ENABLE_SAVE bool BmpFormat::onSave(FileOp *fop) { - const Image* image = fop->sequenceImage(); - const int w = image->width(); - const int h = image->height(); + const FileAbstractImage* img = fop->abstractImage(); + const ImageSpec spec = img->spec(); + const int w = spec.width(); + const int h = spec.height(); int bfSize; int biSizeImage; int ncolors = fop->sequenceGetNColors(); int bpp = 0; - switch (image->pixelFormat()) { - case IMAGE_RGB: + switch (spec.colorMode()) { + case ColorMode::RGB: bpp = 24; break; - case IMAGE_GRAYSCALE: + case ColorMode::GRAYSCALE: bpp = 8; break; - case IMAGE_INDEXED: { + case ColorMode::INDEXED: { if (ncolors > 16) bpp = 8; else if (ncolors > 2) @@ -776,7 +778,7 @@ bool BmpFormat::onSave(FileOp *fop) break; } default: - // TODO save IMAGE_BITMAP as 1bpp bmp? + // TODO save ColorMode::BITMAP as 1bpp bmp? // Invalid image format fop->setError("Unsupported color mode.\n"); return false; @@ -851,31 +853,37 @@ bool BmpFormat::onSave(FileOp *fop) // Save image pixels (from bottom to top) for (i=h-1; i>=0; i--) { - switch (image->pixelFormat()) { - case IMAGE_RGB: + switch (spec.colorMode()) { + case ColorMode::RGB: { + auto scanline = (const uint32_t*)img->getScanline(i); for (j=0; j(image, j, i); + c = scanline[j]; fputc(rgba_getb(c), f); fputc(rgba_getg(c), f); fputc(rgba_getr(c), f); } break; - case IMAGE_GRAYSCALE: + } + case ColorMode::GRAYSCALE: { + auto scanline = (const uint16_t*)img->getScanline(i); for (j=0; j(image, j, i); + c = scanline[j]; fputc(graya_getv(c), f); } break; - case IMAGE_INDEXED: + } + case ColorMode::INDEXED: { + auto scanline = (const uint8_t*)img->getScanline(i); for (j=0; j=0 && j(image, j, i); + c = scanline[j]; value |= (c & colorMask) << (bpp*k); } fputc(value, f); } break; + } } for (j=0; jsequenceImage(); + const ImageRef image = fop->sequenceImage(); int x, y, c, r, g, b, a, alpha; const auto css_options = std::static_pointer_cast(fop->formatOptions()); FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb")); @@ -160,7 +160,7 @@ bool CssFormat::onSave(FileOp* fop) case IMAGE_RGB: { for (y=0; yheight(); y++) { for (x=0; xwidth(); x++) { - c = get_pixel_fast(image, x, y); + c = get_pixel_fast(image.get(), x, y); alpha = rgba_geta(c); if (alpha != 0x00) { print_shadow_color(x, y, rgba_getr(c), rgba_getg(c), rgba_getb(c), @@ -175,7 +175,7 @@ bool CssFormat::onSave(FileOp* fop) case IMAGE_GRAYSCALE: { for (y=0; yheight(); y++) { for (x=0; xwidth(); x++) { - c = get_pixel_fast(image, x, y); + c = get_pixel_fast(image.get(), x, y); auto v = graya_getv(c); alpha = graya_geta(c); if (alpha != 0x00) { @@ -204,7 +204,7 @@ bool CssFormat::onSave(FileOp* fop) } for (y=0; yheight(); y++) { for (x=0; xwidth(); x++) { - c = get_pixel_fast(image, x, y); + c = get_pixel_fast(image.get(), x, y); if (c != mask_color) { if (css_options->withVars) { print_shadow_index(x, y, c, num_printed_pixels>0); diff --git a/src/app/file/file.cpp b/src/app/file/file.cpp index 7baf57582..cef7fe3d2 100644 --- a/src/app/file/file.cpp +++ b/src/app/file/file.cpp @@ -34,6 +34,7 @@ #include "base/scoped_lock.h" #include "base/string.h" #include "dio/detect_format.h" +#include "doc/algorithm/resize_image.h" #include "doc/doc.h" #include "fmt/format.h" #include "render/quantization.h" @@ -53,6 +54,130 @@ namespace app { using namespace base; +class FileOp::FileAbstractImageImpl : public FileAbstractImage { +public: + FileAbstractImageImpl(FileOp* fop) + : m_doc(fop->document()) + , m_sprite(m_doc->sprite()) + , m_spec(m_sprite->spec()) + , m_newBlend(fop->newBlend()) { + ASSERT(m_doc && m_sprite); + } + + void setSliceBounds(const gfx::Rect& sliceBounds) { + m_spec.setWidth(sliceBounds.w * m_scale.x); + m_spec.setHeight(sliceBounds.h * m_scale.y); + } + + void setUnscaledImage(const doc::frame_t frame, + const doc::ImageRef& image) { + if (m_spec.width() == image->width() && + m_spec.height() == image->height()) { + m_tmpScaledImage = image; + } + else { + if (!m_tmpScaledImage) + m_tmpScaledImage.reset(doc::Image::create(m_spec)); + + doc::algorithm::resize_image( + image.get(), + m_tmpScaledImage.get(), + doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR, + palette(frame), + m_sprite->rgbMap(frame), + image->maskColor()); + } + } + + // FileAbstractImage impl + doc::ImageSpec spec() const override { + return m_spec; + } + + os::ColorSpaceRef osColorSpace() const override { + return m_doc->osColorSpace(); + } + + bool needAlpha() const override { + return m_sprite->needAlpha(); + } + + bool isOpaque() const override { + return m_sprite->isOpaque(); + } + + int frames() const override { + return m_sprite->totalFrames(); + } + + int frameDuration(doc::frame_t frame) const override { + return m_sprite->frameDuration(frame); + } + + const doc::Palette* palette(doc::frame_t frame) const override { + ASSERT(m_sprite); + return m_sprite->palette(frame); + } + + doc::PalettesList palettes() const override { + ASSERT(m_sprite); + return m_sprite->getPalettes(); + } + + const doc::ImageRef getScaledImage() const override { + return m_tmpScaledImage; + } + + const uint8_t* getScanline(int y) const override { + return m_tmpScaledImage->getPixelAddress(0, y); + } + + void renderFrame(const doc::frame_t frame, doc::Image* dst) const override { + const bool needResize = + (dst->width() != m_sprite->width() || + dst->height() != m_sprite->height()); + + if (needResize && !m_tmpUnscaledRender) { + auto spec = m_sprite->spec(); + spec.setColorMode(dst->colorMode()); + m_tmpUnscaledRender.reset(doc::Image::create(spec)); + } + + render::Render render; + render.setNewBlend(m_newBlend); + render.setBgType(render::BgType::NONE); + + render.renderSprite( + (needResize ? m_tmpUnscaledRender.get(): dst), + m_sprite, frame); + + if (needResize) { + doc::algorithm::resize_image( + m_tmpUnscaledRender.get(), + dst, + doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR, + palette(frame), + m_sprite->rgbMap(frame), + m_tmpUnscaledRender->maskColor()); + } + } + + void setScale(const gfx::PointF& scale) { + m_scale = scale; + m_spec.setWidth(m_spec.width() * m_scale.x); + m_spec.setHeight(m_spec.height() * m_scale.y); + } + +private: + const Doc* m_doc; + const doc::Sprite* m_sprite; + doc::ImageSpec m_spec; + bool m_newBlend; + doc::ImageRef m_tmpScaledImage = nullptr; + mutable doc::ImageRef m_tmpUnscaledRender = nullptr; + gfx::PointF m_scale = gfx::PointF(1.0, 1.0); +}; + base::paths get_readable_extensions() { base::paths paths; @@ -586,6 +711,18 @@ FileOp* FileOp::createSaveDocumentOperation(const Context* context, return fop.release(); } +// static +bool FileOp::checkIfFormatSupportResizeOnTheFly(const std::string& filename) +{ + // Get the format through the extension of the filename + FileFormat* fileFormat = + FileFormatsManager::instance()->getFileFormat( + dio::detect_format_by_file_extension(filename)); + + return (fileFormat && + fileFormat->support(FILE_ENCODE_ABSTRACT_IMAGE)); +} + // Executes the file operation: loads or saves the sprite. // // It can be called from a different thread of the one used @@ -774,6 +911,9 @@ void FileOp::operate(IFileOpProgress* progress) if (!key || key->isEmpty()) continue; // Skip frame because there is no slice key + if (m_abstractImage) + m_abstractImage->setSliceBounds(key->bounds()); + m_seq.image.reset( Image::create(sprite->pixelFormat(), key->bounds().w, @@ -1080,7 +1220,7 @@ void FileOp::sequenceGetAlpha(int index, int* a) const *a = 0; } -Image* FileOp::sequenceImage(PixelFormat pixelFormat, int w, int h) +ImageRef FileOp::sequenceImage(PixelFormat pixelFormat, int w, int h) { Sprite* sprite; @@ -1118,7 +1258,33 @@ Image* FileOp::sequenceImage(PixelFormat pixelFormat, int w, int h) m_seq.image.reset(Image::create(pixelFormat, w, h)); m_seq.last_cel = new Cel(m_seq.frame++, ImageRef(nullptr)); - return m_seq.image.get(); + return m_seq.image; +} + +void FileOp::makeAbstractImage() +{ + ASSERT(m_format->support(FILE_ENCODE_ABSTRACT_IMAGE)); + if (!m_abstractImage) + m_abstractImage = std::make_unique(this); +} + +FileAbstractImage* FileOp::abstractImage() +{ + ASSERT(m_format->support(FILE_ENCODE_ABSTRACT_IMAGE)); + + makeAbstractImage(); + + // Use sequenceImage() to fill the current image + if (m_format->support(FILE_SUPPORT_SEQUENCES)) + m_abstractImage->setUnscaledImage(m_seq.frame, sequenceImage()); + + return m_abstractImage.get(); +} + +void FileOp::setOnTheFlyScale(const gfx::PointF& scale) +{ + makeAbstractImage(); + m_abstractImage->setScale(scale); } void FileOp::setError(const char *format, ...) diff --git a/src/app/file/file.h b/src/app/file/file.h index 9e9385a7b..5a91d35eb 100644 --- a/src/app/file/file.h +++ b/src/app/file/file.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2021 Igara Studio S.A. +// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -20,6 +20,7 @@ #include "doc/image_ref.h" #include "doc/pixel_format.h" #include "doc/selected_frames.h" +#include "os/color_space.h" #include #include @@ -94,6 +95,33 @@ namespace app { doc::SelectedFrames m_selFrames; }; + // Used by file formats with FILE_ENCODE_ABSTRACT_IMAGE flag, to + // encode a sprite with an intermediate transformation on-the-fly + // (e.g. resizing). + class FileAbstractImage { + public: + virtual ~FileAbstractImage() { } + virtual doc::ImageSpec spec() const = 0; + virtual os::ColorSpaceRef osColorSpace() const = 0; + virtual bool needAlpha() const = 0; + virtual bool isOpaque() const = 0; + virtual int frames() const = 0; + virtual int frameDuration(doc::frame_t frame) const = 0; + + virtual const doc::Palette* palette(doc::frame_t frame) const = 0; + virtual doc::PalettesList palettes() const = 0; + + virtual const doc::ImageRef getScaledImage() const = 0; + + // In case the file format can encode scanline by scanline + // (e.g. PNG format). + virtual const uint8_t* getScanline(int y) const = 0; + + // In case that the encoder needs full frame renders (or compare + // between frames), e.g. GIF format. + virtual void renderFrame(const doc::frame_t frame, doc::Image* dst) const = 0; + }; + // Structure to load & save files. // // TODO This class do to many things. There should be a previous @@ -113,6 +141,8 @@ namespace app { const std::string& filenameFormat, const bool ignoreEmptyFrames); + static bool checkIfFormatSupportResizeOnTheFly(const std::string& filename); + ~FileOp(); bool isSequence() const { return !m_seq.filename_list.empty(); } @@ -189,8 +219,8 @@ namespace app { void sequenceGetColor(int index, int* r, int* g, int* b) const; void sequenceSetAlpha(int index, int a); void sequenceGetAlpha(int index, int* a) const; - Image* sequenceImage(PixelFormat pixelFormat, int w, int h); - const Image* sequenceImage() const { return m_seq.image.get(); } + ImageRef sequenceImage(PixelFormat pixelFormat, int w, int h); + const ImageRef sequenceImage() const { return m_seq.image; } const Palette* sequenceGetPalette() const { return m_seq.palette; } bool sequenceGetHasAlpha() const { return m_seq.has_alpha; @@ -202,6 +232,11 @@ namespace app { return m_seq.flags; } + // Can be used to encode sequences/static files (e.g. png files) + // or animations (e.g. gif) resizing the result on the fly. + FileAbstractImage* abstractImage(); + void setOnTheFlyScale(const gfx::PointF& scale); + const std::string& error() const { return m_error; } void setError(const char *error, ...); bool hasError() const { return !m_error.empty(); } @@ -276,7 +311,11 @@ namespace app { int flags; } m_seq; + class FileAbstractImageImpl; + std::unique_ptr m_abstractImage; + void prepareForSequence(); + void makeAbstractImage(); }; // Available extensions for each load/save operation. diff --git a/src/app/file/file_format.h b/src/app/file/file_format.h index 0346a5007..6503d1552 100644 --- a/src/app/file/file_format.h +++ b/src/app/file/file_format.h @@ -30,6 +30,7 @@ #define FILE_SUPPORT_TAGS 0x00001000 #define FILE_SUPPORT_BIG_PALETTES 0x00002000 // Palettes w/more than 256 colors #define FILE_SUPPORT_PALETTE_WITH_ALPHA 0x00004000 +#define FILE_ENCODE_ABSTRACT_IMAGE 0x00008000 // Use the new FileAbstractImage namespace app { diff --git a/src/app/file/gif_format.cpp b/src/app/file/gif_format.cpp index 2c1ef628a..e918013d9 100644 --- a/src/app/file/gif_format.cpp +++ b/src/app/file/gif_format.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2020 Igara Studio S.A. +// Copyright (C) 2018-2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -92,7 +92,8 @@ class GifFormat : public FileFormat { FILE_SUPPORT_INDEXED | FILE_SUPPORT_FRAMES | FILE_SUPPORT_PALETTES | - FILE_SUPPORT_GET_FORMAT_OPTIONS; + FILE_SUPPORT_GET_FORMAT_OPTIONS | + FILE_ENCODE_ABSTRACT_IMAGE; } bool onLoad(FileOp* fop) override; @@ -899,14 +900,15 @@ public: GifEncoder(FileOp* fop, GifFileType* gifFile) : m_fop(fop) , m_gifFile(gifFile) - , m_document(fop->document()) , m_sprite(fop->document()->sprite()) - , m_spriteBounds(m_sprite->bounds()) - , m_hasBackground(m_sprite->backgroundLayer() ? true: false) + , m_img(fop->abstractImage()) + , m_spec(m_img->spec()) + , m_spriteBounds(m_spec.bounds()) + , m_hasBackground(m_img->isOpaque()) , m_bitsPerPixel(1) , m_globalColormap(nullptr) , m_quantizeColormaps(false) { - if (m_sprite->pixelFormat() == IMAGE_INDEXED) { + if (m_spec.colorMode() == ColorMode::INDEXED) { for (Palette* palette : m_sprite->getPalettes()) { int bpp = GifBitSizeLimited(palette->size()); m_bitsPerPixel = std::max(m_bitsPerPixel, bpp); @@ -916,8 +918,8 @@ public: m_bitsPerPixel = 8; } - if (m_sprite->pixelFormat() == IMAGE_INDEXED && - m_sprite->getPalettes().size() == 1) { + if (m_spec.colorMode() == ColorMode::INDEXED && + m_img->palettes().size() == 1) { // If some layer has opacity < 255 or a different blend mode, we // need to create color palettes. for (const Layer* layer : m_sprite->allVisibleLayers()) { @@ -933,7 +935,7 @@ public: if (!m_quantizeColormaps) { m_globalColormap = createColorMap(m_sprite->palette(0)); - m_bgIndex = m_sprite->transparentColor(); + m_bgIndex = m_spec.maskColor(); } else m_bgIndex = 0; @@ -1089,7 +1091,7 @@ private: const DisposalMethod disposalMethod, const bool fixDuration) { unsigned char extension_bytes[5]; - int frameDelay = m_sprite->frameDuration(frame) / 10; + int frameDelay = m_img->frameDuration(frame) / 10; // Fix duration for Twitter. It looks like the last frame must be // 1/4 of its duration for some strange reason in the Twitter @@ -1356,12 +1358,8 @@ private: } void renderFrame(frame_t frame, Image* dst) { - render::Render render; - render.setNewBlend(m_fop->newBlend()); - - render.setBgType(render::BgType::NONE); clear_image(dst, m_clearColor); - render.renderSprite(dst, m_sprite, frame); + m_img->renderFrame(frame, dst); } private: @@ -1371,8 +1369,7 @@ private: ColorMapObject* colormap = GifMakeMapObject(n, nullptr); // Color space conversions - ConvertCS convert = convert_from_custom_to_srgb( - m_document->osColorSpace()); + ConvertCS convert = convert_from_custom_to_srgb(m_img->osColorSpace()); for (int i=0; isequenceImage( + ImageRef image = fop->sequenceImage( (dinfo.out_color_space == JCS_RGB ? IMAGE_RGB: IMAGE_GRAYSCALE), dinfo.output_width, @@ -352,7 +353,8 @@ bool JpegFormat::onSave(FileOp* fop) { struct jpeg_compress_struct cinfo; struct error_mgr jerr; - const Image* image = fop->sequenceImage(); + const FileAbstractImage* img = fop->abstractImage(); + const ImageSpec spec = img->spec(); JSAMPARRAY buffer; JDIMENSION buffer_height; const auto jpeg_options = std::static_pointer_cast(fop->formatOptions()); @@ -377,10 +379,10 @@ bool JpegFormat::onSave(FileOp* fop) jpeg_stdio_dest(&cinfo, file); // SET parameters for compression. - cinfo.image_width = image->width(); - cinfo.image_height = image->height(); + cinfo.image_width = spec.width(); + cinfo.image_height = spec.height(); - if (image->pixelFormat() == IMAGE_GRAYSCALE) { + if (spec.colorMode() == ColorMode::GRAYSCALE) { cinfo.input_components = 1; cinfo.in_color_space = JCS_GRAYSCALE; } @@ -427,15 +429,15 @@ bool JpegFormat::onSave(FileOp* fop) // Write each scan line. while (cinfo.next_scanline < cinfo.image_height) { // RGB - if (image->pixelFormat() == IMAGE_RGB) { + if (spec.colorMode() == ColorMode::RGB) { uint32_t* src_address; uint8_t* dst_address; int x, y; for (y=0; y<(int)buffer_height; y++) { - src_address = (uint32_t*)image->getPixelAddress(0, cinfo.next_scanline+y); + src_address = (uint32_t*)img->getScanline(cinfo.next_scanline+y); dst_address = ((uint8_t**)buffer)[y]; - for (x=0; xwidth(); ++x) { + for (x=0; xgetPixelAddress(0, cinfo.next_scanline+y); + src_address = (uint16_t*)img->getScanline(cinfo.next_scanline+y); dst_address = ((uint8_t**)buffer)[y]; - for (x=0; xwidth(); ++x) + for (x=0; xsequenceImage(bpp == 8 ? - IMAGE_INDEXED: - IMAGE_RGB, - width, height); + ImageRef image = fop->sequenceImage(bpp == 8 ? + IMAGE_INDEXED: + IMAGE_RGB, + width, height); if (!image) { return false; } if (bpp == 24) - clear_image(image, rgba(0, 0, 0, 255)); + clear_image(image.get(), rgba(0, 0, 0, 255)); for (y=0; ywidth()) - put_pixel_fast(image, x, y, ch); + put_pixel_fast(image.get(), x, y, ch); x++; } @@ -139,8 +141,8 @@ bool PcxFormat::onLoad(FileOp* fop) else { while (c--) { if (xx < image->width()) - put_pixel_fast(image, xx, y, - get_pixel_fast(image, xx, y) | ((ch & 0xff) << po)); + put_pixel_fast(image.get(), xx, y, + get_pixel_fast(image.get(), xx, y) | ((ch & 0xff) << po)); x++; if (x == bytes_per_line) { @@ -190,7 +192,8 @@ bool PcxFormat::onLoad(FileOp* fop) #ifdef ENABLE_SAVE bool PcxFormat::onSave(FileOp* fop) { - const Image* image = fop->sequenceImage(); + const FileAbstractImage* img = fop->abstractImage(); + const ImageSpec spec = img->spec(); int c, r, g, b; int x, y; int runcount; @@ -201,7 +204,7 @@ bool PcxFormat::onSave(FileOp* fop) FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb")); FILE* f = handle.get(); - if (image->pixelFormat() == IMAGE_RGB) { + if (spec.colorMode() == ColorMode::RGB) { depth = 24; planes = 3; } @@ -216,8 +219,8 @@ bool PcxFormat::onSave(FileOp* fop) fputc(8, f); /* 8 bits per pixel */ fputw(0, f); /* xmin */ fputw(0, f); /* ymin */ - fputw(image->width()-1, f); /* xmax */ - fputw(image->height()-1, f); /* ymax */ + fputw(spec.width()-1, f); /* xmax */ + fputw(spec.height()-1, f); /* ymax */ fputw(320, f); /* HDpi */ fputw(200, f); /* VDpi */ @@ -230,36 +233,39 @@ bool PcxFormat::onSave(FileOp* fop) fputc(0, f); /* reserved */ fputc(planes, f); /* one or three color planes */ - fputw(image->width(), f); /* number of bytes per scanline */ + fputw(spec.width(), f); /* number of bytes per scanline */ fputw(1, f); /* color palette */ - fputw(image->width(), f); /* hscreen size */ - fputw(image->height(), f); /* vscreen size */ + fputw(spec.width(), f); /* hscreen size */ + fputw(spec.height(), f); /* vscreen size */ for (c=0; c<54; c++) /* filler */ fputc(0, f); - for (y=0; yheight(); y++) { /* for each scanline... */ + for (y=0; ywidth()*planes; x++) { /* for each pixel... */ + + const uint8_t* scanline = img->getScanline(y); + + for (x=0; xpixelFormat() == IMAGE_INDEXED) - ch = get_pixel_fast(image, x, y); - else if (image->pixelFormat() == IMAGE_GRAYSCALE) { - c = get_pixel_fast(image, x, y); + if (spec.colorMode() == ColorMode::INDEXED) + ch = scanline[x]; + else if (spec.colorMode() == ColorMode::GRAYSCALE) { + c = ((const uint16_t*)scanline)[x]; ch = graya_getv(c); } } else { - if (x < image->width()) { - c = get_pixel_fast(image, x, y); + if (x < spec.width()) { + c = ((const uint32_t*)scanline)[x]; ch = rgba_getr(c); } - else if (xwidth()*2) { - c = get_pixel_fast(image, x-image->width(), y); + else if (x(image, x-image->width()*2, y); + c = ((const uint32_t*)scanline)[x-spec.width()*2]; ch = rgba_getb(c); } } @@ -285,7 +291,7 @@ bool PcxFormat::onSave(FileOp* fop) fputc(runchar, f); - fop->setProgress((float)(y+1) / (float)(image->height())); + fop->setProgress((float)(y+1) / (float)(spec.height())); } if (depth == 8) { /* 256 color palette */ diff --git a/src/app/file/png_format.cpp b/src/app/file/png_format.cpp index 3f456dd2b..60e2f06f0 100644 --- a/src/app/file/png_format.cpp +++ b/src/app/file/png_format.cpp @@ -54,7 +54,8 @@ class PngFormat : public FileFormat { FILE_SUPPORT_GRAYA | FILE_SUPPORT_INDEXED | FILE_SUPPORT_SEQUENCES | - FILE_SUPPORT_PALETTE_WITH_ALPHA; + FILE_SUPPORT_PALETTE_WITH_ALPHA | + FILE_ENCODE_ABSTRACT_IMAGE; } bool onLoad(FileOp* fop) override; @@ -275,7 +276,7 @@ bool PngFormat::onLoad(FileOp* fop) int imageWidth = png_get_image_width(png, info); int imageHeight = png_get_image_height(png, info); - Image* image = fop->sequenceImage(pixelFormat, imageWidth, imageHeight); + ImageRef image = fop->sequenceImage(pixelFormat, imageWidth, imageHeight); if (!image) { fop->setError("file_sequence_image %dx%d\n", imageWidth, imageHeight); return false; @@ -550,23 +551,25 @@ bool PngFormat::onSave(FileOp* fop) png_init_io(png, fp); - const Image* image = fop->sequenceImage(); - switch (image->pixelFormat()) { - case IMAGE_RGB: + const FileAbstractImage* img = fop->abstractImage(); + const ImageSpec spec = img->spec(); + + switch (spec.colorMode()) { + case ColorMode::RGB: color_type = - (fop->document()->sprite()->needAlpha() || + (img->needAlpha() || fix_one_alpha_pixel ? PNG_COLOR_TYPE_RGB_ALPHA: PNG_COLOR_TYPE_RGB); break; - case IMAGE_GRAYSCALE: + case ColorMode::GRAYSCALE: color_type = - (fop->document()->sprite()->needAlpha() || + (img->needAlpha() || fix_one_alpha_pixel ? PNG_COLOR_TYPE_GRAY_ALPHA: PNG_COLOR_TYPE_GRAY); break; - case IMAGE_INDEXED: + case ColorMode::INDEXED: if (fix_one_alpha_pixel) color_type = PNG_COLOR_TYPE_RGB_ALPHA; else @@ -574,8 +577,8 @@ bool PngFormat::onSave(FileOp* fop) break; } - const png_uint_32 width = image->width(); - const png_uint_32 height = image->height(); + const png_uint_32 width = spec.width(); + const png_uint_32 height = spec.height(); png_set_IHDR(png, info, width, height, 8, color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); @@ -606,9 +609,9 @@ bool PngFormat::onSave(FileOp* fop) png_set_unknown_chunks(png, info, &unknowns[0], num_unknowns); } - if (fop->preserveColorProfile() && - fop->document()->sprite()->colorSpace()) - saveColorSpace(png, info, fop->document()->sprite()->colorSpace().get()); + if (fop->preserveColorProfile() && spec.colorSpace()) { + saveColorSpace(png, info, spec.colorSpace().get()); + } if (color_type == PNG_COLOR_TYPE_PALETTE) { int c, r, g, b; @@ -633,7 +636,7 @@ bool PngFormat::onSave(FileOp* fop) // If the sprite does not have a (visible) background layer, we // put alpha=0 to the transparent color. int mask_entry = -1; - if (fop->document()->sprite()->backgroundLayer() == NULL || + if (fop->document()->sprite()->backgroundLayer() == nullptr || !fop->document()->sprite()->backgroundLayer()->isVisible()) { mask_entry = fop->document()->sprite()->transparentColor(); } @@ -668,8 +671,8 @@ bool PngFormat::onSave(FileOp* fop) unsigned int x, c, a; bool opaque = true; - if (image->pixelFormat() == IMAGE_RGB) { - uint32_t* src_address = (uint32_t*)image->getPixelAddress(0, y); + if (spec.colorMode() == ColorMode::RGB) { + auto src_address = (const uint32_t*)img->getScanline(y); for (x=0; xpixelFormat() == IMAGE_INDEXED) { - uint8_t* src_address = (uint8_t*)image->getPixelAddress(0, y); + else if (spec.colorMode() == ColorMode::INDEXED) { + auto src_address = (const uint8_t*)img->getScanline(y); unsigned int x, c; int r, g, b, a; bool opaque = true; @@ -716,7 +719,7 @@ bool PngFormat::onSave(FileOp* fop) } } else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_RGB) { - uint32_t* src_address = (uint32_t*)image->getPixelAddress(0, y); + auto src_address = (const uint32_t*)img->getScanline(y); unsigned int x, c; for (x=0; xgetPixelAddress(0, y); + auto src_address = (const uint16_t*)img->getScanline(y); unsigned int x, c, a; bool opaque = true; @@ -747,7 +750,7 @@ bool PngFormat::onSave(FileOp* fop) } } else if (png_get_color_type(png, info) == PNG_COLOR_TYPE_GRAY) { - uint16_t* src_address = (uint16_t*)image->getPixelAddress(0, y); + auto src_address = (const uint16_t*)img->getScanline(y); unsigned int x, c; for (x=0; xgetPixelAddress(0, y); + auto src_address = (const uint8_t*)img->getScanline(y); unsigned int x; for (x=0; xpixelFormat() == IMAGE_INDEXED) { + if (spec.colorMode() == ColorMode::INDEXED) { png_free(png, palette); palette = nullptr; } diff --git a/src/app/file/svg_format.cpp b/src/app/file/svg_format.cpp index 6e49cfc15..91b49fc4c 100644 --- a/src/app/file/svg_format.cpp +++ b/src/app/file/svg_format.cpp @@ -81,7 +81,7 @@ bool SvgFormat::onLoad(FileOp* fop) bool SvgFormat::onSave(FileOp* fop) { - const Image* image = fop->sequenceImage(); + const ImageRef image = fop->sequenceImage(); int x, y, c, r, g, b, a, alpha; const auto svg_options = std::static_pointer_cast(fop->formatOptions()); const int pixelScaleValue = std::clamp(svg_options->pixelScale, 0, 10000); @@ -103,7 +103,7 @@ bool SvgFormat::onSave(FileOp* fop) case IMAGE_RGB: { for (y=0; yheight(); y++) { for (x=0; xwidth(); x++) { - c = get_pixel_fast(image, x, y); + c = get_pixel_fast(image.get(), x, y); alpha = rgba_geta(c); if (alpha != 0x00) printcol(x, y, rgba_getr(c), rgba_getg(c), rgba_getb(c), alpha, pixelScaleValue); @@ -115,7 +115,7 @@ bool SvgFormat::onSave(FileOp* fop) case IMAGE_GRAYSCALE: { for (y=0; yheight(); y++) { for (x=0; xwidth(); x++) { - c = get_pixel_fast(image, x, y); + c = get_pixel_fast(image.get(), x, y); auto v = graya_getv(c); alpha = graya_geta(c); if (alpha != 0x00) @@ -142,7 +142,7 @@ bool SvgFormat::onSave(FileOp* fop) } for (y=0; yheight(); y++) { for (x=0; xwidth(); x++) { - c = get_pixel_fast(image, x, y); + c = get_pixel_fast(image.get(), x, y); if (c != mask_color) printcol(x, y, image_palette[c][0] & 0xff, image_palette[c][1] & 0xff, diff --git a/src/app/file/tga_format.cpp b/src/app/file/tga_format.cpp index bc2a4a7e8..fe418dfc2 100644 --- a/src/app/file/tga_format.cpp +++ b/src/app/file/tga_format.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019-2021 Igara Studio S.A. +// Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -53,7 +53,8 @@ class TgaFormat : public FileFormat { FILE_SUPPORT_INDEXED | FILE_SUPPORT_SEQUENCES | FILE_SUPPORT_GET_FORMAT_OPTIONS | - FILE_SUPPORT_PALETTE_WITH_ALPHA; + FILE_SUPPORT_PALETTE_WITH_ALPHA | + FILE_ENCODE_ABSTRACT_IMAGE; } bool onLoad(FileOp* fop) override; @@ -164,9 +165,9 @@ bool TgaFormat::onLoad(FileOp* fop) if (decoder.hasAlpha()) fop->sequenceSetHasAlpha(true); - Image* image = fop->sequenceImage((doc::PixelFormat)spec.colorMode(), - spec.width(), - spec.height()); + ImageRef image = fop->sequenceImage((doc::PixelFormat)spec.colorMode(), + spec.width(), + spec.height()); if (!image) return false; @@ -188,7 +189,7 @@ bool TgaFormat::onLoad(FileOp* fop) // Post process gray image pixels (because we use grayscale images // with alpha). if (header.isGray()) { - doc::LockImageBits bits(image); + doc::LockImageBits bits(image.get()); for (auto it=bits.begin(), end=bits.end(); it != end; ++it) { *it = doc::graya(*it, 255); } @@ -217,7 +218,7 @@ bool TgaFormat::onLoad(FileOp* fop) namespace { void prepare_header(tga::Header& header, - const doc::Image* image, + const doc::ImageSpec& spec, const doc::Palette* palette, const bool isOpaque, const bool compressed, @@ -231,13 +232,13 @@ void prepare_header(tga::Header& header, header.colormapDepth = 0; header.xOrigin = 0; header.yOrigin = 0; - header.width = image->width(); - header.height = image->height(); + header.width = spec.width(); + header.height = spec.height(); header.bitsPerPixel = 0; // TODO make this option configurable header.imageDescriptor = 0x20; // Top-to-bottom - switch (image->colorMode()) { + switch (spec.colorMode()) { case ColorMode::RGB: header.imageType = (compressed ? tga::RleRgb: tga::UncompressedRgb); header.bitsPerPixel = (bitsPerPixel > 8 ? @@ -287,7 +288,7 @@ void prepare_header(tga::Header& header, bool TgaFormat::onSave(FileOp* fop) { - const Image* image = fop->sequenceImage(); + const FileAbstractImage* img = fop->abstractImage(); const Palette* palette = fop->sequenceGetPalette(); FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb")); @@ -297,7 +298,7 @@ bool TgaFormat::onSave(FileOp* fop) const auto tgaOptions = std::static_pointer_cast(fop->formatOptions()); prepare_header( - header, image, palette, + header, img->spec(), palette, // Is alpha channel required? fop->document()->sprite()->isOpaque(), // Compressed by default @@ -307,6 +308,7 @@ bool TgaFormat::onSave(FileOp* fop) encoder.writeHeader(header); + doc::ImageRef image = img->getScaledImage(); tga::Image tgaImage; tgaImage.pixels = image->getPixelAddress(0, 0); tgaImage.rowstride = image->getRowStrideSize(); diff --git a/src/app/ui/export_file_window.cpp b/src/app/ui/export_file_window.cpp index f84de4983..ce0c2c4ed 100644 --- a/src/app/ui/export_file_window.cpp +++ b/src/app/ui/export_file_window.cpp @@ -141,6 +141,12 @@ bool ExportFileWindow::isForTwitter() const return forTwitter()->isSelected(); } +void ExportFileWindow::setResizeScale(const gfx::PointF& scale) +{ + resize()->setValue( + base::convert_to(scale.x)); // TODO support x & y +} + void ExportFileWindow::setAniDir(const doc::AniDir aniDir) { anidir()->setSelectedItemIndex(int(aniDir)); diff --git a/src/app/ui/export_file_window.h b/src/app/ui/export_file_window.h index 80b8c29a1..b8840090e 100644 --- a/src/app/ui/export_file_window.h +++ b/src/app/ui/export_file_window.h @@ -34,12 +34,13 @@ namespace app { bool applyPixelRatio() const; bool isForTwitter() const; + void setOutputFilename(const std::string& pathAndFilename); + void setResizeScale(const gfx::PointF& scale); void setAniDir(const doc::AniDir aniDir); obs::signal SelectOutputFile; private: - void setOutputFilename(const std::string& pathAndFilename); void updateOutputFilenameEntry(); void onOutputFilenameEntryChange(); void updateAniDir(); From af74f8bc84a527d1e1a6ed8f15b4c7203bf2830a Mon Sep 17 00:00:00 2001 From: David Capello Date: Tue, 14 Jun 2022 23:25:10 -0300 Subject: [PATCH 8/9] Fix memory leak when the created cel is not used in ExpandCelCanvas --- src/app/util/expand_cel_canvas.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/util/expand_cel_canvas.cpp b/src/app/util/expand_cel_canvas.cpp index 8ea1f77ae..add09ff04 100644 --- a/src/app/util/expand_cel_canvas.cpp +++ b/src/app/util/expand_cel_canvas.cpp @@ -182,6 +182,11 @@ void ExpandCelCanvas::commit() // And finally we add the cel again in the layer. m_transaction.execute(new cmd::AddCel(m_layer, m_cel)); } + else { + // Delete unused cel + delete m_cel; + m_cel = nullptr; + } } // We are selecting inside a layer group... else { From b7d5d4a2c9ea7ed4c9f7bc9ba665039d808ad3b6 Mon Sep 17 00:00:00 2001 From: David Capello Date: Wed, 15 Jun 2022 12:25:31 -0300 Subject: [PATCH 9/9] Editable resize factor in File > Export (fix #3007) Related to #3008 --- data/widgets/export_file.xml | 28 +++++++++++++++------------- src/app/commands/cmd_save_file.cpp | 19 +++++++++++-------- src/app/ui/export_file_window.cpp | 11 +++++------ src/app/ui/export_file_window.h | 2 +- src/app/widget_loader.cpp | 4 ++++ src/ui/combobox.cpp | 11 ++++++++++- 6 files changed, 46 insertions(+), 29 deletions(-) diff --git a/data/widgets/export_file.xml b/data/widgets/export_file.xml index 38e2ab7fb..b05c25fad 100644 --- a/data/widgets/export_file.xml +++ b/data/widgets/export_file.xml @@ -1,4 +1,5 @@ + @@ -8,19 +9,20 @@