mirror of https://github.com/aseprite/aseprite.git
Merge 7d99ade754
into 6d89a6bc15
This commit is contained in:
commit
ca1e99bf70
|
@ -16,20 +16,23 @@
|
||||||
#include "app/commands/params.h"
|
#include "app/commands/params.h"
|
||||||
#include "app/console.h"
|
#include "app/console.h"
|
||||||
#include "app/doc.h"
|
#include "app/doc.h"
|
||||||
|
#include "app/extensions.h"
|
||||||
#include "app/file/file.h"
|
#include "app/file/file.h"
|
||||||
#include "app/file_selector.h"
|
#include "app/file_selector.h"
|
||||||
#include "app/i18n/strings.h"
|
#include "app/i18n/strings.h"
|
||||||
#include "app/modules/gui.h"
|
|
||||||
#include "app/pref/preferences.h"
|
#include "app/pref/preferences.h"
|
||||||
#include "app/recent_files.h"
|
#include "app/recent_files.h"
|
||||||
#include "app/ui/status_bar.h"
|
#include "app/ui/status_bar.h"
|
||||||
#include "app/ui_context.h"
|
#include "app/ui_context.h"
|
||||||
#include "app/util/open_file_job.h"
|
#include "app/util/open_file_job.h"
|
||||||
#include "base/fs.h"
|
#include "base/fs.h"
|
||||||
#include "base/thread.h"
|
|
||||||
#include "doc/sprite.h"
|
#include "doc/sprite.h"
|
||||||
#include "ui/ui.h"
|
#include "ui/ui.h"
|
||||||
|
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
#include "app/script/security.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
@ -142,6 +145,13 @@ void OpenFileCommand::onExecute(Context* context)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (fop->hasError()) {
|
if (fop->hasError()) {
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
if (fop->hasUnknownFormatError() && loadCustomFormat(filename)) {
|
||||||
|
// If a script was detected and loaded the file without errors, then we can return safely
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
console.printf(fop->error().c_str());
|
console.printf(fop->error().c_str());
|
||||||
unrecent = true;
|
unrecent = true;
|
||||||
}
|
}
|
||||||
|
@ -230,6 +240,32 @@ std::string OpenFileCommand::onGetFriendlyName() const
|
||||||
m_filename))));
|
m_filename))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
bool OpenFileCommand::loadCustomFormat(const std::string& filename)
|
||||||
|
{
|
||||||
|
const std::string detectedExtension = base::string_to_lower(base::get_file_extension(filename));
|
||||||
|
if (detectedExtension.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (const auto& customFormatExtension : App::instance()->extensions().customFormatList()) {
|
||||||
|
if (base::string_to_lower(customFormatExtension) == detectedExtension) {
|
||||||
|
for (Extension* extension : App::instance()->extensions()) {
|
||||||
|
if (!extension->isEnabled() || !extension->hasFileFormats())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto formatId = extension->getCustomFormatIdForExtension(customFormatExtension,
|
||||||
|
Extension::FileFormat::Load);
|
||||||
|
if (formatId.has_value()) {
|
||||||
|
return extension->loadCustomFormat(*formatId, filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
Command* CommandFactory::createOpenFileCommand()
|
Command* CommandFactory::createOpenFileCommand()
|
||||||
{
|
{
|
||||||
return new OpenFileCommand;
|
return new OpenFileCommand;
|
||||||
|
|
|
@ -32,6 +32,10 @@ protected:
|
||||||
std::string onGetFriendlyName() const override;
|
std::string onGetFriendlyName() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
bool loadCustomFormat(const std::string& filename);
|
||||||
|
#endif
|
||||||
|
|
||||||
std::string m_filename;
|
std::string m_filename;
|
||||||
std::string m_folder;
|
std::string m_folder;
|
||||||
bool m_ui;
|
bool m_ui;
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
#include "app/context_access.h"
|
#include "app/context_access.h"
|
||||||
#include "app/doc.h"
|
#include "app/doc.h"
|
||||||
#include "app/doc_undo.h"
|
#include "app/doc_undo.h"
|
||||||
|
#include "app/extensions.h"
|
||||||
#include "app/file/file.h"
|
#include "app/file/file.h"
|
||||||
#include "app/file/gif_format.h"
|
#include "app/file/gif_format.h"
|
||||||
#include "app/file/png_format.h"
|
#include "app/file/png_format.h"
|
||||||
|
@ -207,12 +208,12 @@ void SaveFileBaseCommand::saveDocumentInBackground(const Context* context,
|
||||||
bounds = document->sprite()->bounds();
|
bounds = document->sprite()->bounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
FileOpROI roi(document,
|
const FileOpROI roi(document,
|
||||||
bounds,
|
bounds,
|
||||||
params().slice(),
|
params().slice(),
|
||||||
params().tag(),
|
params().tag(),
|
||||||
m_framesSeq,
|
m_framesSeq,
|
||||||
m_adjustFramesByTag);
|
m_adjustFramesByTag);
|
||||||
|
|
||||||
std::unique_ptr<FileOp> fop(FileOp::createSaveDocumentOperation(context,
|
std::unique_ptr<FileOp> fop(FileOp::createSaveDocumentOperation(context,
|
||||||
roi,
|
roi,
|
||||||
|
@ -222,13 +223,23 @@ void SaveFileBaseCommand::saveDocumentInBackground(const Context* context,
|
||||||
if (!fop)
|
if (!fop)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
bool runJob = true;
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
if (fop->hasUnknownFormatError() &&
|
||||||
|
saveCustomFormat(context, roi, filename, params().filenameFormat(), params().ignoreEmpty())) {
|
||||||
|
runJob = false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (resizeOnTheFly == ResizeOnTheFly::On)
|
if (resizeOnTheFly == ResizeOnTheFly::On)
|
||||||
fop->setOnTheFlyScale(scale);
|
fop->setOnTheFlyScale(scale);
|
||||||
|
|
||||||
SaveFileJob job(fop.get(), params().ui());
|
if (runJob) {
|
||||||
job.showProgressWindow();
|
SaveFileJob job(fop.get(), params().ui());
|
||||||
|
job.showProgressWindow();
|
||||||
|
}
|
||||||
|
|
||||||
if (fop->hasError()) {
|
if (fop->hasError() && runJob) {
|
||||||
Console console;
|
Console console;
|
||||||
console.printf(fop->error().c_str());
|
console.printf(fop->error().c_str());
|
||||||
|
|
||||||
|
@ -259,6 +270,49 @@ void SaveFileBaseCommand::saveDocumentInBackground(const Context* context,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
bool SaveFileBaseCommand::saveCustomFormat(const Context* context,
|
||||||
|
const FileOpROI& roi,
|
||||||
|
const std::string& filename,
|
||||||
|
const std::string& filenameFormatArg,
|
||||||
|
bool ignoreEmptyFrames)
|
||||||
|
{
|
||||||
|
const std::string detectedExtension = base::string_to_lower(base::get_file_extension(filename));
|
||||||
|
if (detectedExtension.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (const auto& customFormatExtension : App::instance()->extensions().customFormatList()) {
|
||||||
|
if (base::string_to_lower(customFormatExtension) == detectedExtension) {
|
||||||
|
for (Extension* extension : App::instance()->extensions()) {
|
||||||
|
if (!extension->isEnabled() || !extension->hasFileFormats())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto formatId = extension->getCustomFormatIdForExtension(customFormatExtension,
|
||||||
|
Extension::FileFormat::Save);
|
||||||
|
if (formatId.has_value()) {
|
||||||
|
// Generate the options struct
|
||||||
|
Extension::FileFormatSaveOptions opts;
|
||||||
|
opts.canvasSize = roi.fileCanvasSize();
|
||||||
|
opts.bounds = roi.bounds();
|
||||||
|
opts.frames = roi.frames();
|
||||||
|
opts.fromFrame = roi.fromFrame();
|
||||||
|
opts.toFrame = roi.toFrame();
|
||||||
|
opts.ignoreEmptyFrames = ignoreEmptyFrames;
|
||||||
|
|
||||||
|
// TODO: ¿FramesSquence & frameBounds?
|
||||||
|
return extension->saveCustomFormat(*formatId,
|
||||||
|
filename,
|
||||||
|
context->activeDocument()->sprite(),
|
||||||
|
opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
class SaveFileCommand : public SaveFileBaseCommand {
|
class SaveFileCommand : public SaveFileBaseCommand {
|
||||||
|
|
|
@ -18,6 +18,9 @@
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
class FileOpROI;
|
||||||
|
}
|
||||||
namespace app {
|
namespace app {
|
||||||
class Doc;
|
class Doc;
|
||||||
|
|
||||||
|
@ -85,6 +88,13 @@ protected:
|
||||||
const MarkAsSaved markAsSaved,
|
const MarkAsSaved markAsSaved,
|
||||||
const ResizeOnTheFly resizeOnTheFly = ResizeOnTheFly::Off,
|
const ResizeOnTheFly resizeOnTheFly = ResizeOnTheFly::Off,
|
||||||
const gfx::PointF& scale = gfx::PointF(1.0, 1.0));
|
const gfx::PointF& scale = gfx::PointF(1.0, 1.0));
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
bool saveCustomFormat(const Context* context,
|
||||||
|
const FileOpROI& roi,
|
||||||
|
const std::string& filename,
|
||||||
|
const std::string& filenameFormatArg,
|
||||||
|
bool ignoreEmptyFrames);
|
||||||
|
#endif
|
||||||
|
|
||||||
doc::FramesSequence m_framesSeq;
|
doc::FramesSequence m_framesSeq;
|
||||||
bool m_adjustFramesByTag;
|
bool m_adjustFramesByTag;
|
||||||
|
|
|
@ -40,15 +40,20 @@
|
||||||
|
|
||||||
#include "archive.h"
|
#include "archive.h"
|
||||||
#include "archive_entry.h"
|
#include "archive_entry.h"
|
||||||
|
#include "doc.h"
|
||||||
#include "json11.hpp"
|
#include "json11.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <queue>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "base/log.h"
|
#include "base/log.h"
|
||||||
|
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
#include "script/docobj.h"
|
||||||
|
#include "script/security.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
const char* Extension::kAsepriteDefaultThemeExtensionName = "aseprite-theme";
|
const char* Extension::kAsepriteDefaultThemeExtensionName = "aseprite-theme";
|
||||||
|
@ -358,6 +363,28 @@ void Extension::addMenuSeparator(ui::Widget* widget)
|
||||||
m_plugin.items.push_back(item);
|
m_plugin.items.push_back(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Extension::addFileFormat(const FileFormat& format)
|
||||||
|
{
|
||||||
|
// Check for any duplicates in global extension list for both ID and file extensions.
|
||||||
|
for (const auto& extension : App::instance()->extensions()) {
|
||||||
|
if (m_fileFormats.count(format.id) > 0)
|
||||||
|
throw base::Exception("Duplicated format ID: " + format.id);
|
||||||
|
|
||||||
|
for (const auto& [id, other] : extension->m_fileFormats) {
|
||||||
|
for (const auto& otherExt : other.extensions) {
|
||||||
|
for (const auto& ourExt : format.extensions) {
|
||||||
|
if (otherExt == ourExt)
|
||||||
|
throw base::Exception(
|
||||||
|
"Extension attempting to register a custom format extension that already exists: " +
|
||||||
|
format.id + " : " + ourExt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_fileFormats.try_emplace(format.id, format);
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool Extension::canBeDisabled() const
|
bool Extension::canBeDisabled() const
|
||||||
|
@ -769,6 +796,147 @@ void Extension::addScript(const std::string& fn)
|
||||||
updateCategory(Category::Scripts);
|
updateCategory(Category::Scripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> Extension::getCustomFormatIdForExtension(
|
||||||
|
const std::string& ext,
|
||||||
|
const FileFormat::Support support) const
|
||||||
|
{
|
||||||
|
ASSERT(support > 0);
|
||||||
|
|
||||||
|
if (!hasFileFormats())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
for (const auto& [id, format] : m_fileFormats) {
|
||||||
|
for (const auto& formatExtensionString : format.extensions) {
|
||||||
|
if (formatExtensionString == ext && format.supports(support)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Extension::loadCustomFormat(const std::string& formatId, const std::string& filename) const
|
||||||
|
{
|
||||||
|
const auto& format = m_fileFormats.at(formatId);
|
||||||
|
ASSERT(format.onsaveRef > -1);
|
||||||
|
|
||||||
|
if (!format.supports(FileFormat::Load))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
script::Engine* engine = App::instance()->scriptEngine();
|
||||||
|
lua_State* L = engine->luaState();
|
||||||
|
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, format.onloadRef);
|
||||||
|
lua_pushstring(L, filename.c_str());
|
||||||
|
|
||||||
|
lua_pushcclosure(L, script::get_original_io_open(), 0);
|
||||||
|
lua_pushstring(L, filename.c_str());
|
||||||
|
lua_pushstring(L, format.binary ? "rb" : "r");
|
||||||
|
|
||||||
|
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
|
||||||
|
Console().printf("Failed to open file for reading");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
|
||||||
|
Console().printf("Failed to call onload function, custom format '%s'", format.id.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lua_toboolean(L, -1) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Extension::saveCustomFormat(const std::string& formatId,
|
||||||
|
const std::string& filename,
|
||||||
|
const Sprite* sprite,
|
||||||
|
const FileFormatSaveOptions& saveOptions) const
|
||||||
|
{
|
||||||
|
const auto& format = m_fileFormats.at(formatId);
|
||||||
|
ASSERT(format.onsaveRef > -1);
|
||||||
|
|
||||||
|
if (!format.supports(FileFormat::Save))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// TODO: Probably something nice to have in base, with a version that doesn't use rand() and isn't
|
||||||
|
// just copy-pasted from StackOverflow :^]
|
||||||
|
auto random_temp_file = []() {
|
||||||
|
auto randchar = []() -> char {
|
||||||
|
const char charset[] = "0123456789"
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
"abcdefghijklmnopqrstuvwxyz";
|
||||||
|
const size_t max_index = (sizeof(charset) - 1);
|
||||||
|
return charset[rand() % max_index];
|
||||||
|
};
|
||||||
|
std::string str(12, 0);
|
||||||
|
std::generate_n(str.begin(), 12, randchar);
|
||||||
|
return "asecmf_" + str + ".tmp";
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::string destTempFile = base::join_path(base::get_temp_path(), random_temp_file());
|
||||||
|
|
||||||
|
script::Engine* engine = App::instance()->scriptEngine();
|
||||||
|
lua_State* L = engine->luaState();
|
||||||
|
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, format.onsaveRef);
|
||||||
|
lua_pushstring(L, filename.c_str());
|
||||||
|
|
||||||
|
lua_pushcclosure(L, script::get_original_io_open(), 0);
|
||||||
|
lua_pushstring(L, destTempFile.c_str());
|
||||||
|
lua_pushstring(L, format.binary ? "wb" : "w");
|
||||||
|
|
||||||
|
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
|
||||||
|
Console().printf("Failed to open file for writing");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
script::push_docobj(L, sprite);
|
||||||
|
|
||||||
|
// Creating push options table.
|
||||||
|
lua_newtable(L);
|
||||||
|
script::push_value_to_lua<gfx::Size>(L, saveOptions.canvasSize);
|
||||||
|
lua_setfield(L, -2, "canvasSize");
|
||||||
|
script::push_value_to_lua<gfx::Rect>(L, saveOptions.bounds);
|
||||||
|
lua_setfield(L, -2, "bounds");
|
||||||
|
script::setfield_integer(L, "frames", saveOptions.frames);
|
||||||
|
script::setfield_integer(L, "fromFrame", saveOptions.fromFrame);
|
||||||
|
script::setfield_integer(L, "toFrame", saveOptions.toFrame);
|
||||||
|
lua_pushboolean(L, saveOptions.ignoreEmptyFrames);
|
||||||
|
lua_setfield(L, -2, "ignoreEmptyFrames");
|
||||||
|
|
||||||
|
if (lua_pcall(L, 4, 1, 0) != LUA_OK) {
|
||||||
|
Console().printf("Failed to call onload function, custom format '%s'", format.id.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lua_toboolean(L, -1) > 0) {
|
||||||
|
// Transfer the temporary file to the actual file location
|
||||||
|
try {
|
||||||
|
// TODO: This is probably *too* cautious? I just don't want a random script we can't control
|
||||||
|
// to delete a user's files.
|
||||||
|
// TODO: Should also be moved to a job.
|
||||||
|
std::string mv;
|
||||||
|
if (base::is_file(filename)) {
|
||||||
|
mv = base::join_path(base::get_temp_path(), base::get_file_name(filename) + ".bak");
|
||||||
|
base::move_file(filename, mv);
|
||||||
|
}
|
||||||
|
|
||||||
|
base::move_file(destTempFile, filename);
|
||||||
|
if (!mv.empty())
|
||||||
|
base::delete_file(mv);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (std::exception& e) {
|
||||||
|
Console().printf("Saving failed: '%s'", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Do we need to close the Lua file handle ourselves if something breaks?
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#endif // ENABLE_SCRIPTING
|
#endif // ENABLE_SCRIPTING
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
@ -923,6 +1091,28 @@ std::vector<Extension::DitheringMatrixInfo*> Extensions::ditheringMatrices()
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
std::vector<std::string> Extensions::customFormatList(
|
||||||
|
const Extension::FileFormat::Support support) const
|
||||||
|
{
|
||||||
|
std::vector<std::string> result;
|
||||||
|
for (auto* ext : m_extensions) {
|
||||||
|
if (!ext->isEnabled())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (const auto& [id, format] : ext->m_fileFormats) {
|
||||||
|
if (!format.supports(support))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (const auto& extension : format.extensions)
|
||||||
|
result.push_back(extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void Extensions::enableExtension(Extension* extension, const bool state)
|
void Extensions::enableExtension(Extension* extension, const bool state)
|
||||||
{
|
{
|
||||||
extension->enable(state);
|
extension->enable(state);
|
||||||
|
|
|
@ -10,17 +10,24 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "app/i18n/lang_info.h"
|
#include "app/i18n/lang_info.h"
|
||||||
|
#include "doc/frame.h"
|
||||||
|
#include "gfx/rect.h"
|
||||||
|
#include "gfx/size.h"
|
||||||
#include "obs/signal.h"
|
#include "obs/signal.h"
|
||||||
#include "render/dithering_matrix.h"
|
#include "render/dithering_matrix.h"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
class Widget;
|
class Widget;
|
||||||
}
|
}
|
||||||
|
namespace doc {
|
||||||
|
class Sprite;
|
||||||
|
}
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
// Key=id
|
// Key=id
|
||||||
|
@ -28,6 +35,7 @@ namespace app {
|
||||||
using ExtensionItems = std::map<std::string, std::string>;
|
using ExtensionItems = std::map<std::string, std::string>;
|
||||||
|
|
||||||
class Extensions;
|
class Extensions;
|
||||||
|
class Doc;
|
||||||
|
|
||||||
struct ExtensionInfo {
|
struct ExtensionInfo {
|
||||||
std::string name;
|
std::string name;
|
||||||
|
@ -83,6 +91,34 @@ public:
|
||||||
ThemeInfo(const std::string& path, const std::string& variant) : path(path), variant(variant) {}
|
ThemeInfo(const std::string& path, const std::string& variant) : path(path), variant(variant) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
struct FileFormat {
|
||||||
|
enum Support : uint8_t { Invalid = 0, Full, Save, Load };
|
||||||
|
std::string id;
|
||||||
|
std::unordered_set<std::string> extensions;
|
||||||
|
bool binary = true;
|
||||||
|
int onloadRef = -1;
|
||||||
|
int onsaveRef = -1;
|
||||||
|
|
||||||
|
bool supports(const Support support) const
|
||||||
|
{
|
||||||
|
if (support == Load && onloadRef > -1)
|
||||||
|
return true;
|
||||||
|
if (support == Save && onsaveRef > -1)
|
||||||
|
return true;
|
||||||
|
return support == Full && onloadRef > -1 && onsaveRef > -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct FileFormatSaveOptions {
|
||||||
|
gfx::Size canvasSize;
|
||||||
|
gfx::Rect bounds;
|
||||||
|
doc::frame_t frames = 0;
|
||||||
|
doc::frame_t fromFrame = 0;
|
||||||
|
doc::frame_t toFrame = 0;
|
||||||
|
bool ignoreEmptyFrames = false;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
using Languages = std::map<std::string, LangInfo>;
|
using Languages = std::map<std::string, LangInfo>;
|
||||||
using Themes = std::map<std::string, ThemeInfo>;
|
using Themes = std::map<std::string, ThemeInfo>;
|
||||||
using DitheringMatrices = std::map<std::string, DitheringMatrixInfo>;
|
using DitheringMatrices = std::map<std::string, DitheringMatrixInfo>;
|
||||||
|
@ -122,6 +158,7 @@ public:
|
||||||
void removeMenuGroup(const std::string& id);
|
void removeMenuGroup(const std::string& id);
|
||||||
|
|
||||||
void addMenuSeparator(ui::Widget* widget);
|
void addMenuSeparator(ui::Widget* widget);
|
||||||
|
void addFileFormat(const FileFormat& format);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool isEnabled() const { return m_isEnabled; }
|
bool isEnabled() const { return m_isEnabled; }
|
||||||
|
@ -137,6 +174,15 @@ public:
|
||||||
#ifdef ENABLE_SCRIPTING
|
#ifdef ENABLE_SCRIPTING
|
||||||
bool hasScripts() const { return !m_plugin.scripts.empty(); }
|
bool hasScripts() const { return !m_plugin.scripts.empty(); }
|
||||||
void addScript(const std::string& fn);
|
void addScript(const std::string& fn);
|
||||||
|
bool hasFileFormats() const { return !m_fileFormats.empty(); }
|
||||||
|
|
||||||
|
std::optional<std::string> getCustomFormatIdForExtension(const std::string& ext,
|
||||||
|
FileFormat::Support support) const;
|
||||||
|
bool loadCustomFormat(const std::string& formatId, const std::string& filename) const;
|
||||||
|
bool saveCustomFormat(const std::string& formatId,
|
||||||
|
const std::string& filename,
|
||||||
|
const doc::Sprite* sprite,
|
||||||
|
const FileFormatSaveOptions& saveOptions) const;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool isCurrentTheme() const;
|
bool isCurrentTheme() const;
|
||||||
|
@ -174,6 +220,7 @@ private:
|
||||||
std::vector<ScriptItem> scripts;
|
std::vector<ScriptItem> scripts;
|
||||||
std::vector<PluginItem> items;
|
std::vector<PluginItem> items;
|
||||||
} m_plugin;
|
} m_plugin;
|
||||||
|
std::unordered_map<std::string, FileFormat> m_fileFormats;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
std::string m_path;
|
std::string m_path;
|
||||||
|
@ -218,6 +265,12 @@ public:
|
||||||
// image file. These pointers cannot be deleted.
|
// image file. These pointers cannot be deleted.
|
||||||
std::vector<Extension::DitheringMatrixInfo*> ditheringMatrices();
|
std::vector<Extension::DitheringMatrixInfo*> ditheringMatrices();
|
||||||
|
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
// Returns a list of the available custom formats (by extension).
|
||||||
|
std::vector<std::string> customFormatList(
|
||||||
|
Extension::FileFormat::Support support = Extension::FileFormat::Support::Full) const;
|
||||||
|
#endif
|
||||||
|
|
||||||
obs::signal<void(Extension*)> NewExtension;
|
obs::signal<void(Extension*)> NewExtension;
|
||||||
obs::signal<void(Extension*)> KeysChange;
|
obs::signal<void(Extension*)> KeysChange;
|
||||||
obs::signal<void(Extension*)> LanguagesChange;
|
obs::signal<void(Extension*)> LanguagesChange;
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
#include "app/context.h"
|
#include "app/context.h"
|
||||||
#include "app/doc.h"
|
#include "app/doc.h"
|
||||||
#include "app/drm.h"
|
#include "app/drm.h"
|
||||||
|
#include "app/extensions.h"
|
||||||
#include "app/file/file_data.h"
|
#include "app/file/file_data.h"
|
||||||
#include "app/file/file_format.h"
|
#include "app/file/file_format.h"
|
||||||
#include "app/file/file_formats_manager.h"
|
#include "app/file/file_formats_manager.h"
|
||||||
|
@ -193,6 +194,14 @@ base::paths get_readable_extensions()
|
||||||
if (format->support(FILE_SUPPORT_LOAD))
|
if (format->support(FILE_SUPPORT_LOAD))
|
||||||
format->getExtensions(paths);
|
format->getExtensions(paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
for (const auto& extension :
|
||||||
|
App::instance()->extensions().customFormatList(Extension::FileFormat::Load)) {
|
||||||
|
paths.push_back(extension);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,6 +213,14 @@ base::paths get_writable_extensions(const int requiredFormatFlag)
|
||||||
(requiredFormatFlag == 0 || format->support(requiredFormatFlag)))
|
(requiredFormatFlag == 0 || format->support(requiredFormatFlag)))
|
||||||
format->getExtensions(paths);
|
format->getExtensions(paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_SCRIPTING
|
||||||
|
for (const auto& extension :
|
||||||
|
App::instance()->extensions().customFormatList(Extension::FileFormat::Load)) {
|
||||||
|
paths.push_back(extension);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,6 +383,7 @@ FileOp* FileOp::createLoadDocumentOperation(Context* context,
|
||||||
// Get the format through the extension of the filename
|
// Get the format through the extension of the filename
|
||||||
fop->m_format = FileFormatsManager::instance()->getFileFormat(dio::detect_format(filename));
|
fop->m_format = FileFormatsManager::instance()->getFileFormat(dio::detect_format(filename));
|
||||||
if (!fop->m_format || !fop->m_format->support(FILE_SUPPORT_LOAD)) {
|
if (!fop->m_format || !fop->m_format->support(FILE_SUPPORT_LOAD)) {
|
||||||
|
fop->setUnknownFormatError(true);
|
||||||
fop->setError("%s can't load \"%s\" file (\"%s\")\n",
|
fop->setError("%s can't load \"%s\" file (\"%s\")\n",
|
||||||
get_app_name(),
|
get_app_name(),
|
||||||
filename.c_str(),
|
filename.c_str(),
|
||||||
|
@ -535,6 +553,7 @@ FileOp* FileOp::createSaveDocumentOperation(const Context* context,
|
||||||
fop->m_format = FileFormatsManager::instance()->getFileFormat(
|
fop->m_format = FileFormatsManager::instance()->getFileFormat(
|
||||||
dio::detect_format_by_file_extension(filename));
|
dio::detect_format_by_file_extension(filename));
|
||||||
if (!fop->m_format || !fop->m_format->support(FILE_SUPPORT_SAVE)) {
|
if (!fop->m_format || !fop->m_format->support(FILE_SUPPORT_SAVE)) {
|
||||||
|
fop->setUnknownFormatError(true);
|
||||||
fop->setError("%s can't save \"%s\" file (\"%s\")\n",
|
fop->setError("%s can't save \"%s\" file (\"%s\")\n",
|
||||||
get_app_name(),
|
get_app_name(),
|
||||||
filename.c_str(),
|
filename.c_str(),
|
||||||
|
@ -1449,6 +1468,7 @@ FileOp::FileOp(FileOpType type, Context* context, const FileOpConfig* config)
|
||||||
, m_createPaletteFromRgba(false)
|
, m_createPaletteFromRgba(false)
|
||||||
, m_ignoreEmpty(false)
|
, m_ignoreEmpty(false)
|
||||||
, m_avoidBackgroundLayer(false)
|
, m_avoidBackgroundLayer(false)
|
||||||
|
, m_unknownFormatError(false)
|
||||||
, m_embeddedColorProfile(false)
|
, m_embeddedColorProfile(false)
|
||||||
, m_embeddedGridBounds(false)
|
, m_embeddedGridBounds(false)
|
||||||
{
|
{
|
||||||
|
|
|
@ -81,6 +81,7 @@ public:
|
||||||
doc::Tag* tag() const { return m_tag; }
|
doc::Tag* tag() const { return m_tag; }
|
||||||
doc::frame_t fromFrame() const { return m_framesSeq.firstFrame(); }
|
doc::frame_t fromFrame() const { return m_framesSeq.firstFrame(); }
|
||||||
doc::frame_t toFrame() const { return m_framesSeq.lastFrame(); }
|
doc::frame_t toFrame() const { return m_framesSeq.lastFrame(); }
|
||||||
|
gfx::Rect bounds() const { return m_bounds; }
|
||||||
const doc::FramesSequence& framesSequence() const { return m_framesSeq; }
|
const doc::FramesSequence& framesSequence() const { return m_framesSeq; }
|
||||||
|
|
||||||
doc::frame_t frames() const { return (doc::frame_t)m_framesSeq.size(); }
|
doc::frame_t frames() const { return (doc::frame_t)m_framesSeq.size(); }
|
||||||
|
@ -259,6 +260,8 @@ public:
|
||||||
bool hasError() const { return !m_error.empty(); }
|
bool hasError() const { return !m_error.empty(); }
|
||||||
void setIncompatibilityError(const std::string& msg);
|
void setIncompatibilityError(const std::string& msg);
|
||||||
bool hasIncompatibilityError() const { return !m_incompatibilityError.empty(); }
|
bool hasIncompatibilityError() const { return !m_incompatibilityError.empty(); }
|
||||||
|
void setUnknownFormatError(bool unknownFormatError) { m_unknownFormatError = unknownFormatError; }
|
||||||
|
bool hasUnknownFormatError() const { return m_unknownFormatError; }
|
||||||
|
|
||||||
double progress() const;
|
double progress() const;
|
||||||
void setProgress(double progress);
|
void setProgress(double progress);
|
||||||
|
@ -305,6 +308,7 @@ private:
|
||||||
bool m_createPaletteFromRgba;
|
bool m_createPaletteFromRgba;
|
||||||
bool m_ignoreEmpty;
|
bool m_ignoreEmpty;
|
||||||
bool m_avoidBackgroundLayer;
|
bool m_avoidBackgroundLayer;
|
||||||
|
bool m_unknownFormatError;
|
||||||
|
|
||||||
// True if the file contained a color profile when it was loaded.
|
// True if the file contained a color profile when it was loaded.
|
||||||
bool m_embeddedColorProfile;
|
bool m_embeddedColorProfile;
|
||||||
|
|
|
@ -300,6 +300,63 @@ int Plugin_newMenuSeparator(lua_State* L)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Plugin_newFileFormat(lua_State* L)
|
||||||
|
{
|
||||||
|
auto* plugin = get_obj<Plugin>(L, 1);
|
||||||
|
if (!lua_istable(L, 2))
|
||||||
|
return luaL_error(L,
|
||||||
|
"plugin:newFileFormat() must be called with a table as its first argument");
|
||||||
|
|
||||||
|
Extension::FileFormat format;
|
||||||
|
|
||||||
|
int type = lua_getfield(L, 2, "id");
|
||||||
|
if (type == LUA_TSTRING) {
|
||||||
|
format.id = lua_tostring(L, -1);
|
||||||
|
if (format.id.empty())
|
||||||
|
return luaL_error(L, "format id is invalid or empty");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return luaL_error(L, "format id must be a string");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
type = lua_getfield(L, 2, "binary");
|
||||||
|
if (type == LUA_TBOOLEAN) {
|
||||||
|
format.binary = lua_toboolean(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
type = lua_getfield(L, 2, "extensions");
|
||||||
|
if (type == LUA_TTABLE) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, -2) != 0) {
|
||||||
|
format.extensions.emplace(base::string_to_lower(luaL_tolstring(L, -1, nullptr)));
|
||||||
|
lua_pop(L, 2);
|
||||||
|
}
|
||||||
|
if (format.extensions.empty())
|
||||||
|
return luaL_error(L, "format extension list is empty");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return luaL_error(L, "format extension list must be a table");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
type = lua_getfield(L, 2, "onload");
|
||||||
|
if (type == LUA_TFUNCTION)
|
||||||
|
format.onloadRef = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||||
|
else
|
||||||
|
return luaL_error(L, "format must have an onload function"); // TODO: Make either of these
|
||||||
|
// optional, so we can only read
|
||||||
|
// and not write and vice-versa.
|
||||||
|
|
||||||
|
type = lua_getfield(L, 2, "onsave");
|
||||||
|
if (type == LUA_TFUNCTION)
|
||||||
|
format.onsaveRef = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||||
|
else
|
||||||
|
return luaL_error(L, "format must have an onsave function");
|
||||||
|
|
||||||
|
plugin->ext->addFileFormat(format);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int Plugin_get_name(lua_State* L)
|
int Plugin_get_name(lua_State* L)
|
||||||
{
|
{
|
||||||
auto* plugin = get_obj<Plugin>(L, 1);
|
auto* plugin = get_obj<Plugin>(L, 1);
|
||||||
|
@ -353,6 +410,7 @@ const luaL_Reg Plugin_methods[] = {
|
||||||
{ "newMenuGroup", Plugin_newMenuGroup },
|
{ "newMenuGroup", Plugin_newMenuGroup },
|
||||||
{ "deleteMenuGroup", Plugin_deleteMenuGroup },
|
{ "deleteMenuGroup", Plugin_deleteMenuGroup },
|
||||||
{ "newMenuSeparator", Plugin_newMenuSeparator },
|
{ "newMenuSeparator", Plugin_newMenuSeparator },
|
||||||
|
{ "newFileFormat", Plugin_newFileFormat },
|
||||||
{ nullptr, nullptr }
|
{ nullptr, nullptr }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -441,4 +441,9 @@ bool ask_access(lua_State* L,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lua_CFunction get_original_io_open()
|
||||||
|
{
|
||||||
|
return replaced_functions[io_open].origfunc;
|
||||||
|
}
|
||||||
|
|
||||||
}} // namespace app::script
|
}} // namespace app::script
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "app/script/engine.h"
|
#include "app/script/engine.h"
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
namespace app { namespace script {
|
namespace app { namespace script {
|
||||||
|
|
||||||
|
@ -35,6 +36,8 @@ enum class ResourceType {
|
||||||
|
|
||||||
void overwrite_unsecure_functions(lua_State* L);
|
void overwrite_unsecure_functions(lua_State* L);
|
||||||
|
|
||||||
|
lua_CFunction get_original_io_open();
|
||||||
|
|
||||||
bool ask_access(lua_State* L,
|
bool ask_access(lua_State* L,
|
||||||
const char* filename,
|
const char* filename,
|
||||||
const FileAccessMode mode,
|
const FileAccessMode mode,
|
||||||
|
|
Loading…
Reference in New Issue