Export slice data in .aseprite-data file (#721)

This commit is contained in:
David Capello 2017-04-12 12:35:13 -03:00
parent 5e1c5af423
commit 80c61ca926
5 changed files with 465 additions and 196 deletions

View File

@ -364,6 +364,7 @@ add_library(app-lib
document_undo.cpp
extra_cel.cpp
file/file.cpp
file/file_data.cpp
file/file_format.cpp
file/file_formats_manager.cpp
file/palette_file.cpp

View File

@ -13,6 +13,7 @@
#include "app/console.h"
#include "app/context.h"
#include "app/document.h"
#include "app/file/file_data.h"
#include "app/file/file_format.h"
#include "app/file/file_formats_manager.h"
#include "app/file/format_options.h"
@ -20,9 +21,7 @@
#include "app/filename_formatter.h"
#include "app/modules/gui.h"
#include "app/modules/palettes.h"
#include "app/pref/preferences.h"
#include "app/ui/status_bar.h"
#include "app/xml_document.h"
#include "base/fs.h"
#include "base/mutex.h"
#include "base/scoped_lock.h"
@ -43,45 +42,6 @@ namespace app {
using namespace base;
namespace {
void updateXmlPartFromSliceKey(const doc::SliceKey* key, TiXmlElement* xmlPart)
{
xmlPart->SetAttribute("x", key->bounds().x);
xmlPart->SetAttribute("y", key->bounds().y);
if (!key->hasCenter()) {
xmlPart->SetAttribute("w", key->bounds().w);
xmlPart->SetAttribute("h", key->bounds().h);
if (xmlPart->Attribute("w1")) xmlPart->RemoveAttribute("w1");
if (xmlPart->Attribute("w2")) xmlPart->RemoveAttribute("w2");
if (xmlPart->Attribute("w3")) xmlPart->RemoveAttribute("w3");
if (xmlPart->Attribute("h1")) xmlPart->RemoveAttribute("h1");
if (xmlPart->Attribute("h2")) xmlPart->RemoveAttribute("h2");
if (xmlPart->Attribute("h3")) xmlPart->RemoveAttribute("h3");
}
else {
xmlPart->SetAttribute("w1", key->center().x);
xmlPart->SetAttribute("w2", key->center().w);
xmlPart->SetAttribute("w3", key->bounds().w - key->center().x2());
xmlPart->SetAttribute("h1", key->center().y);
xmlPart->SetAttribute("h2", key->center().h);
xmlPart->SetAttribute("h3", key->bounds().h - key->center().y2());
if (xmlPart->Attribute("w")) xmlPart->RemoveAttribute("w");
if (xmlPart->Attribute("h")) xmlPart->RemoveAttribute("h");
}
if (key->hasPivot()) {
xmlPart->SetAttribute("focusx", key->pivot().x);
xmlPart->SetAttribute("focusy", key->pivot().y);
}
else {
if (xmlPart->Attribute("focusx")) xmlPart->RemoveAttribute("focusx");
if (xmlPart->Attribute("focusy")) xmlPart->RemoveAttribute("focusy");
}
}
} // anonymous namespace
std::string get_readable_extensions()
{
std::string buf;
@ -723,7 +683,12 @@ void FileOp::operate(IFileOpProgress* progress)
if (m_document &&
m_document->sprite() &&
!m_dataFilename.empty()) {
loadData();
try {
load_aseprite_data_file(m_dataFilename, m_document);
}
catch (const std::exception& ex) {
setError("Error loading data file: %s\n", ex.what());
}
}
}
// Save //////////////////////////////////////////////////////////////////////
@ -805,7 +770,12 @@ void FileOp::operate(IFileOpProgress* progress)
m_document->sprite() &&
!hasError() &&
!m_dataFilename.empty()) {
saveData();
try {
save_aseprite_data_file(m_dataFilename, m_document);
}
catch (const std::exception& ex) {
setError("Error loading data file: %s\n", ex.what());
}
}
#else
setError(
@ -1091,155 +1061,4 @@ void FileOp::prepareForSequence()
m_formatOptions.reset();
}
void FileOp::loadData()
{
try {
XmlDocumentRef xmlDoc = open_xml(m_dataFilename);
TiXmlHandle handle(xmlDoc.get());
TiXmlElement* xmlSlices = handle
.FirstChild("sprite")
.FirstChild("slices").ToElement();
// Update theme.xml file
if (xmlSlices &&
xmlSlices->Attribute("theme")) {
std::string themeFileName = xmlSlices->Attribute("theme");
// Open theme XML file
XmlDocumentRef xmlThemeDoc = open_xml(
base::join_path(base::get_file_path(m_dataFilename), themeFileName));
TiXmlHandle themeHandle(xmlThemeDoc.get());
for (TiXmlElement* xmlPart = themeHandle
.FirstChild("theme")
.FirstChild("parts")
.FirstChild("part").ToElement();
xmlPart;
xmlPart=xmlPart->NextSiblingElement()) {
const char* partId = xmlPart->Attribute("id");
if (!partId)
continue;
auto slice = new doc::Slice();
slice->setName(partId);
// Default slice color
auto color = Preferences::instance().slices.defaultColor();
slice->userData().setColor(
doc::rgba(color.getRed(),
color.getGreen(),
color.getBlue(),
color.getAlpha()));
doc::SliceKey key;
int x = strtol(xmlPart->Attribute("x"), NULL, 10);
int y = strtol(xmlPart->Attribute("y"), NULL, 10);
if (xmlPart->Attribute("w1")) {
int w1 = xmlPart->Attribute("w1") ? strtol(xmlPart->Attribute("w1"), NULL, 10): 0;
int w2 = xmlPart->Attribute("w2") ? strtol(xmlPart->Attribute("w2"), NULL, 10): 0;
int w3 = xmlPart->Attribute("w3") ? strtol(xmlPart->Attribute("w3"), NULL, 10): 0;
int h1 = xmlPart->Attribute("h1") ? strtol(xmlPart->Attribute("h1"), NULL, 10): 0;
int h2 = xmlPart->Attribute("h2") ? strtol(xmlPart->Attribute("h2"), NULL, 10): 0;
int h3 = xmlPart->Attribute("h3") ? strtol(xmlPart->Attribute("h3"), NULL, 10): 0;
key.setBounds(gfx::Rect(x, y, w1+w2+w3, h1+h2+h3));
key.setCenter(gfx::Rect(w1, h1, w2, h2));
}
else if (xmlPart->Attribute("w")) {
int w = xmlPart->Attribute("w") ? strtol(xmlPart->Attribute("w"), NULL, 10): 0;
int h = xmlPart->Attribute("h") ? strtol(xmlPart->Attribute("h"), NULL, 10): 0;
key.setBounds(gfx::Rect(x, y, w, h));
}
if (xmlPart->Attribute("focusx")) {
int x = xmlPart->Attribute("focusx") ? strtol(xmlPart->Attribute("focusx"), NULL, 10): 0;
int y = xmlPart->Attribute("focusy") ? strtol(xmlPart->Attribute("focusy"), NULL, 10): 0;
key.setPivot(gfx::Point(x, y));
}
slice->insert(0, key);
m_document->sprite()->slices().add(slice);
}
}
}
catch (const std::exception& ex) {
setError("Error loading data file: %s\n", ex.what());
}
}
void FileOp::saveData()
{
try {
XmlDocumentRef xmlDoc = open_xml(m_dataFilename);
TiXmlHandle handle(xmlDoc.get());
TiXmlElement* xmlSlices = handle
.FirstChild("sprite")
.FirstChild("slices").ToElement();
if (xmlSlices &&
xmlSlices->Attribute("theme")) {
// Open theme XML file
std::string themeFileName = base::join_path(
base::get_file_path(m_dataFilename), xmlSlices->Attribute("theme"));
XmlDocumentRef xmlThemeDoc = open_xml(themeFileName);
TiXmlHandle themeHandle(xmlThemeDoc.get());
TiXmlElement* xmlNext = nullptr;
std::set<std::string> existent;
TiXmlElement* xmlParts =
themeHandle
.FirstChild("theme")
.FirstChild("parts").ToElement();
for (TiXmlElement* xmlPart=(TiXmlElement*)xmlParts->FirstChild("part");
xmlPart;
xmlPart=xmlNext) {
xmlNext = xmlPart->NextSiblingElement();
const char* partId = xmlPart->Attribute("id");
if (!partId)
continue;
bool found = false;
for (auto slice : m_document->sprite()->slices()) {
const SliceKey* key;
if ((slice->name() == partId) &&
(key = slice->getByFrame(0))) {
existent.insert(slice->name());
updateXmlPartFromSliceKey(key, xmlPart);
found = true;
break;
}
}
// Delete this <part> element (as the slice was removed)
if (!found)
xmlPart->Parent()->RemoveChild(xmlPart);
}
for (auto slice : m_document->sprite()->slices()) {
const SliceKey* key;
if (existent.find(slice->name()) == existent.end() &&
(key = slice->getByFrame(0))) {
TiXmlElement xmlPart("part");
xmlPart.SetAttribute("id", slice->name().c_str());
updateXmlPartFromSliceKey(key, &xmlPart);
xmlParts->InsertAfterChild(xmlParts->LastChild(), xmlPart);
}
}
save_xml(xmlThemeDoc, themeFileName);
}
}
catch (const std::exception& ex) {
setError("Error loading data file: %s\n", ex.what());
}
}
} // namespace app

View File

@ -212,8 +212,6 @@ namespace app {
} m_seq;
void prepareForSequence();
void loadData();
void saveData();
};
// Available extensions for each load/save operation.

425
src/app/file/file_data.cpp Normal file
View File

@ -0,0 +1,425 @@
// Aseprite
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/file/file_data.h"
#include "app/pref/preferences.h"
#include "app/xml_document.h"
#include "base/convert_to.h"
#include "base/fs.h"
#include "doc/color.h"
#include "doc/document.h"
#include "doc/slice.h"
#include <cstdlib>
#include <set>
namespace app {
using namespace base;
namespace {
std::string color_to_hex(doc::color_t color)
{
char buf[256];
if (doc::rgba_geta(color) == 255) {
std::sprintf(buf, "#%02x%02x%02x",
doc::rgba_getr(color),
doc::rgba_getg(color),
doc::rgba_getb(color));
}
else {
std::sprintf(buf, "#%02x%02x%02x%02x",
doc::rgba_getr(color),
doc::rgba_getg(color),
doc::rgba_getb(color),
doc::rgba_geta(color));
}
return buf;
}
doc::color_t color_from_hex(const char* str)
{
if (*str == '#')
++str;
if (std::strlen(str) == 3) { // #fff
uint32_t value = std::strtol(str, nullptr, 16);
int r = ((value & 0xf00) >> 8);
int g = ((value & 0xf0) >> 4);
int b = (value & 0xf);
return gfx::rgba(
r | (r << 4),
g | (g << 4),
b | (b << 4));
}
else if (std::strlen(str) == 6) { // #ffffff
uint32_t value = std::strtol(str, nullptr, 16);
return gfx::rgba(
(value & 0xff0000) >> 16,
(value & 0xff00) >> 8,
(value & 0xff));
}
else if (std::strlen(str) == 8) { // #ffffffff
uint32_t value = std::strtol(str, nullptr, 16);
return gfx::rgba(
(value & 0xff000000) >> 24,
(value & 0xff0000) >> 16,
(value & 0xff00) >> 8,
(value & 0xff));
}
else
return gfx::rgba(0, 0, 0, 255);
}
template<typename Container,
typename ChildNameGetterFunc,
typename UpdateXmlChildFunc>
void update_xml_collection(Container& container,
TiXmlElement* xmlParent,
const char* childElemName,
const char* idAttrName,
ChildNameGetterFunc childNameGetter,
UpdateXmlChildFunc updateXmlChild)
{
if (!xmlParent)
return;
TiXmlElement* xmlNext = nullptr;
std::set<std::string> existent;
// Update existent children
for (TiXmlElement* xmlChild=(xmlParent->FirstChild(childElemName) ?
xmlParent->FirstChild(childElemName)->ToElement(): nullptr);
xmlChild;
xmlChild=xmlNext) {
xmlNext = xmlChild->NextSiblingElement();
const char* xmlChildName = xmlChild->Attribute(idAttrName);
if (!xmlChildName)
continue;
bool found = false;
for (auto child : container) {
std::string thisChildName = childNameGetter(child);
if (thisChildName == xmlChildName) {
existent.insert(thisChildName);
updateXmlChild(child, xmlChild);
found = true;
break;
}
}
// Delete this <child> element (as the child was removed from the
// original container)
if (!found)
xmlParent->RemoveChild(xmlChild);
}
// Add new children
for (auto child : container) {
std::string thisChildName = childNameGetter(child);
if (existent.find(thisChildName) == existent.end()) {
TiXmlElement xmlChild(childElemName);
xmlChild.SetAttribute(idAttrName, thisChildName.c_str());
updateXmlChild(child, &xmlChild);
xmlParent->InsertEndChild(xmlChild);
}
}
}
void update_xml_part_from_slice_key(const doc::SliceKey* key, TiXmlElement* xmlPart)
{
xmlPart->SetAttribute("x", key->bounds().x);
xmlPart->SetAttribute("y", key->bounds().y);
if (!key->hasCenter()) {
xmlPart->SetAttribute("w", key->bounds().w);
xmlPart->SetAttribute("h", key->bounds().h);
if (xmlPart->Attribute("w1")) xmlPart->RemoveAttribute("w1");
if (xmlPart->Attribute("w2")) xmlPart->RemoveAttribute("w2");
if (xmlPart->Attribute("w3")) xmlPart->RemoveAttribute("w3");
if (xmlPart->Attribute("h1")) xmlPart->RemoveAttribute("h1");
if (xmlPart->Attribute("h2")) xmlPart->RemoveAttribute("h2");
if (xmlPart->Attribute("h3")) xmlPart->RemoveAttribute("h3");
}
else {
xmlPart->SetAttribute("w1", key->center().x);
xmlPart->SetAttribute("w2", key->center().w);
xmlPart->SetAttribute("w3", key->bounds().w - key->center().x2());
xmlPart->SetAttribute("h1", key->center().y);
xmlPart->SetAttribute("h2", key->center().h);
xmlPart->SetAttribute("h3", key->bounds().h - key->center().y2());
if (xmlPart->Attribute("w")) xmlPart->RemoveAttribute("w");
if (xmlPart->Attribute("h")) xmlPart->RemoveAttribute("h");
}
if (key->hasPivot()) {
xmlPart->SetAttribute("focusx", key->pivot().x);
xmlPart->SetAttribute("focusy", key->pivot().y);
}
else {
if (xmlPart->Attribute("focusx")) xmlPart->RemoveAttribute("focusx");
if (xmlPart->Attribute("focusy")) xmlPart->RemoveAttribute("focusy");
}
}
void update_xml_slice(const doc::Slice* slice, TiXmlElement* xmlSlice)
{
if (!slice->userData().text().empty())
xmlSlice->SetAttribute("text", slice->userData().text().c_str());
else if (xmlSlice->Attribute("text"))
xmlSlice->RemoveAttribute("text");
xmlSlice->SetAttribute("color", color_to_hex(slice->userData().color()).c_str());
// Update <key> elements
update_xml_collection(
*slice,
xmlSlice, "key", "frame",
[](const Keyframes<SliceKey>::Key& key) -> std::string {
return base::convert_to<std::string>(key.frame());
},
[](const Keyframes<SliceKey>::Key& key, TiXmlElement* xmlKey) {
SliceKey* sliceKey = key.value();
xmlKey->SetAttribute("x", sliceKey->bounds().x);
xmlKey->SetAttribute("y", sliceKey->bounds().y);
xmlKey->SetAttribute("w", sliceKey->bounds().w);
xmlKey->SetAttribute("h", sliceKey->bounds().h);
if (sliceKey->hasCenter()) {
xmlKey->SetAttribute("cx", sliceKey->center().x);
xmlKey->SetAttribute("cy", sliceKey->center().y);
xmlKey->SetAttribute("cw", sliceKey->center().w);
xmlKey->SetAttribute("ch", sliceKey->center().h);
}
else {
if (xmlKey->Attribute("cx")) xmlKey->RemoveAttribute("cx");
if (xmlKey->Attribute("cy")) xmlKey->RemoveAttribute("cy");
if (xmlKey->Attribute("cw")) xmlKey->RemoveAttribute("cw");
if (xmlKey->Attribute("ch")) xmlKey->RemoveAttribute("ch");
}
if (sliceKey->hasPivot()) {
xmlKey->SetAttribute("px", sliceKey->pivot().x);
xmlKey->SetAttribute("py", sliceKey->pivot().y);
}
else {
if (xmlKey->Attribute("px")) xmlKey->RemoveAttribute("px");
if (xmlKey->Attribute("py")) xmlKey->RemoveAttribute("py");
}
});
}
} // anonymous namespace
void load_aseprite_data_file(const std::string& dataFilename, doc::Document* doc)
{
XmlDocumentRef xmlDoc = open_xml(dataFilename);
TiXmlHandle handle(xmlDoc.get());
TiXmlElement* xmlSlices = handle
.FirstChild("sprite")
.FirstChild("slices").ToElement();
// Load slices/parts from theme.xml file
if (xmlSlices &&
xmlSlices->Attribute("theme")) {
std::string themeFileName = xmlSlices->Attribute("theme");
// Open theme XML file
XmlDocumentRef xmlThemeDoc = open_xml(
base::join_path(base::get_file_path(dataFilename), themeFileName));
TiXmlHandle themeHandle(xmlThemeDoc.get());
for (TiXmlElement* xmlPart = themeHandle
.FirstChild("theme")
.FirstChild("parts")
.FirstChild("part").ToElement();
xmlPart;
xmlPart=xmlPart->NextSiblingElement()) {
const char* partId = xmlPart->Attribute("id");
if (!partId)
continue;
auto slice = new doc::Slice();
slice->setName(partId);
// Default slice color
auto color = Preferences::instance().slices.defaultColor();
slice->userData().setColor(
doc::rgba(color.getRed(),
color.getGreen(),
color.getBlue(),
color.getAlpha()));
doc::SliceKey key;
int x = strtol(xmlPart->Attribute("x"), NULL, 10);
int y = strtol(xmlPart->Attribute("y"), NULL, 10);
if (xmlPart->Attribute("w1")) {
int w1 = xmlPart->Attribute("w1") ? strtol(xmlPart->Attribute("w1"), NULL, 10): 0;
int w2 = xmlPart->Attribute("w2") ? strtol(xmlPart->Attribute("w2"), NULL, 10): 0;
int w3 = xmlPart->Attribute("w3") ? strtol(xmlPart->Attribute("w3"), NULL, 10): 0;
int h1 = xmlPart->Attribute("h1") ? strtol(xmlPart->Attribute("h1"), NULL, 10): 0;
int h2 = xmlPart->Attribute("h2") ? strtol(xmlPart->Attribute("h2"), NULL, 10): 0;
int h3 = xmlPart->Attribute("h3") ? strtol(xmlPart->Attribute("h3"), NULL, 10): 0;
key.setBounds(gfx::Rect(x, y, w1+w2+w3, h1+h2+h3));
key.setCenter(gfx::Rect(w1, h1, w2, h2));
}
else if (xmlPart->Attribute("w")) {
int w = xmlPart->Attribute("w") ? strtol(xmlPart->Attribute("w"), NULL, 10): 0;
int h = xmlPart->Attribute("h") ? strtol(xmlPart->Attribute("h"), NULL, 10): 0;
key.setBounds(gfx::Rect(x, y, w, h));
}
if (xmlPart->Attribute("focusx")) {
int x = xmlPart->Attribute("focusx") ? strtol(xmlPart->Attribute("focusx"), NULL, 10): 0;
int y = xmlPart->Attribute("focusy") ? strtol(xmlPart->Attribute("focusy"), NULL, 10): 0;
key.setPivot(gfx::Point(x, y));
}
slice->insert(0, key);
doc->sprite()->slices().add(slice);
}
}
// Load slices from <slice> elements
else if (xmlSlices) {
for (TiXmlElement* xmlSlice=(xmlSlices->FirstChild("slice") ?
xmlSlices->FirstChild("slice")->ToElement(): nullptr);
xmlSlice;
xmlSlice=xmlSlice->NextSiblingElement()) {
const char* sliceId = xmlSlice->Attribute("id");
if (!sliceId)
continue;
// If the document already contains a slice with this name, use the one from the document
if (doc->sprite()->slices().getByName(sliceId))
continue;
auto slice = new doc::Slice();
slice->setName(sliceId);
// Slice text
if (xmlSlice->Attribute("text"))
slice->userData().setText(xmlSlice->Attribute("text"));
// Slice color
doc::color_t color;
if (xmlSlice->Attribute("color")) {
color = color_from_hex(xmlSlice->Attribute("color"));
}
else {
app::Color appColor = Preferences::instance().slices.defaultColor();
color = doc::rgba(appColor.getRed(),
appColor.getGreen(),
appColor.getBlue(),
appColor.getAlpha());
}
slice->userData().setColor(color);
for (TiXmlElement* xmlKey=(xmlSlice->FirstChild("key") ?
xmlSlice->FirstChild("key")->ToElement(): nullptr);
xmlKey;
xmlKey=xmlKey->NextSiblingElement()) {
if (!xmlKey->Attribute("frame"))
continue;
doc::SliceKey key;
doc::frame_t frame = std::strtol(xmlKey->Attribute("frame"), nullptr, 10);
int x = std::strtol(xmlKey->Attribute("x"), nullptr, 10);
int y = std::strtol(xmlKey->Attribute("y"), nullptr, 10);
int w = std::strtol(xmlKey->Attribute("w"), nullptr, 10);
int h = std::strtol(xmlKey->Attribute("h"), nullptr, 10);
key.setBounds(gfx::Rect(x, y, w, h));
if (xmlKey->Attribute("cx")) {
int cx = std::strtol(xmlKey->Attribute("cx"), nullptr, 10);
int cy = std::strtol(xmlKey->Attribute("cy"), nullptr, 10);
int cw = std::strtol(xmlKey->Attribute("cw"), nullptr, 10);
int ch = std::strtol(xmlKey->Attribute("ch"), nullptr, 10);
key.setCenter(gfx::Rect(cx, cy, cw, ch));
}
if (xmlKey->Attribute("px")) {
int px = std::strtol(xmlKey->Attribute("px"), nullptr, 10);
int py = std::strtol(xmlKey->Attribute("py"), nullptr, 10);
key.setPivot(gfx::Point(px, py));
}
slice->insert(frame, key);
}
doc->sprite()->slices().add(slice);
}
}
}
#ifdef ENABLE_SAVE
void save_aseprite_data_file(const std::string& dataFilename, const doc::Document* doc)
{
XmlDocumentRef xmlDoc = open_xml(dataFilename);
TiXmlHandle handle(xmlDoc.get());
TiXmlElement* xmlSlices = handle
.FirstChild("sprite")
.FirstChild("slices").ToElement();
// Update theme.xml file
if (xmlSlices &&
xmlSlices->Attribute("theme")) {
// Open theme XML file
std::string themeFileName = base::join_path(
base::get_file_path(dataFilename), xmlSlices->Attribute("theme"));
XmlDocumentRef xmlThemeDoc = open_xml(themeFileName);
TiXmlHandle themeHandle(xmlThemeDoc.get());
TiXmlElement* xmlParts =
themeHandle
.FirstChild("theme")
.FirstChild("parts").ToElement();
update_xml_collection(
doc->sprite()->slices(),
xmlParts, "part", "id",
[](const Slice* slice) -> std::string {
if (slice->getByFrame(0))
return slice->name();
else
return std::string();
},
[](Slice* slice, TiXmlElement* xmlSlice) {
ASSERT(slice->getByFrame(0));
update_xml_part_from_slice_key(slice->getByFrame(0), xmlSlice);
});
// Save theme.xml file
save_xml(xmlThemeDoc, themeFileName);
}
// <slices> without "theme" attribute
else if (xmlSlices) {
update_xml_collection(
doc->sprite()->slices(),
xmlSlices, "slice", "id",
[](const Slice* slice) -> std::string {
return slice->name();
},
update_xml_slice);
// Save .aseprite-data file
save_xml(xmlDoc, dataFilename);
}
}
#endif
} // namespace app

26
src/app/file/file_data.h Normal file
View File

@ -0,0 +1,26 @@
// Aseprite
// Copyright (C) 2017 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_FILE_FILE_DATA_H_INCLUDED
#define APP_FILE_FILE_DATA_H_INCLUDED
#pragma once
#include <string>
namespace doc {
class Document;
}
namespace app {
void load_aseprite_data_file(const std::string& dataFilename, doc::Document* doc);
#ifdef ENABLE_SAVE
void save_aseprite_data_file(const std::string& dataFilename, const doc::Document* doc);
#endif
} // namespace app
#endif