Simple implementation of issue #17 - Export animation to JSON/sprite sheet files

This is a good start point. From here we can add more formats and options
(XML, custom formats, templates, etc.), rotated sprites, trim, etc.)
This commit is contained in:
David Capello 2013-12-08 20:19:32 -03:00
parent d77efb602e
commit f531f6d0d0
8 changed files with 516 additions and 5 deletions

View File

@ -100,6 +100,7 @@ add_library(app-library
data_recovery.cpp data_recovery.cpp
document.cpp document.cpp
document_api.cpp document_api.cpp
document_exporter.cpp
document_location.cpp document_location.cpp
document_undo.cpp document_undo.cpp
documents.cpp documents.cpp

View File

@ -29,6 +29,7 @@
#include "app/commands/params.h" #include "app/commands/params.h"
#include "app/console.h" #include "app/console.h"
#include "app/data_recovery.h" #include "app/data_recovery.h"
#include "app/document_exporter.h"
#include "app/document_location.h" #include "app/document_location.h"
#include "app/document_observer.h" #include "app/document_observer.h"
#include "app/drop_files.h" #include "app/drop_files.h"
@ -112,6 +113,7 @@ App::App(int argc, const char* argv[])
, m_legacy(NULL) , m_legacy(NULL)
, m_isGui(false) , m_isGui(false)
, m_isShell(false) , m_isShell(false)
, m_exporter(NULL)
{ {
ASSERT(m_instance == NULL); ASSERT(m_instance == NULL);
m_instance = this; m_instance = this;
@ -124,6 +126,14 @@ App::App(int argc, const char* argv[])
m_legacy = new LegacyModules(isGui() ? REQUIRE_INTERFACE: 0); m_legacy = new LegacyModules(isGui() ? REQUIRE_INTERFACE: 0);
m_files = options.files(); m_files = options.files();
if (options.hasExporterParams()) {
m_exporter.reset(new DocumentExporter);
m_exporter->setDataFilename(options.data());
m_exporter->setTextureFilename(options.sheet());
m_exporter->setScale(options.scale());
}
// Register well-known image file types. // Register well-known image file types.
FileFormatsManager::instance().registerAllFormats(); FileFormatsManager::instance().registerAllFormats();
@ -186,6 +196,7 @@ int App::run()
PRINTF("Processing options...\n"); PRINTF("Processing options...\n");
{ {
UIContext* context = UIContext::instance();
Console console; Console console;
for (FileList::iterator for (FileList::iterator
it = m_files.begin(), it = m_files.begin(),
@ -199,15 +210,27 @@ int App::run()
} }
else { else {
// Mount and select the sprite // Mount and select the sprite
UIContext* context = UIContext::instance();
context->addDocument(document); context->addDocument(document);
if (isGui()) { // Add the given file in the argument as a "recent file" only
// Recent file // if we are running in GUI mode. If the program is executed
// in batch mode this is not desirable.
if (isGui())
getRecentFiles()->addRecentFile(it->c_str()); getRecentFiles()->addRecentFile(it->c_str());
// Add the document to the exporter.
if (m_exporter != NULL)
m_exporter->addDocument(document);
} }
} }
} }
// Export
if (m_exporter != NULL) {
PRINTF("Exporting sheet...\n");
m_exporter->exportSheet();
m_exporter.reset(NULL);
} }
// Run the GUI // Run the GUI

View File

@ -39,6 +39,7 @@ namespace raster {
namespace app { namespace app {
class Document; class Document;
class DocumentExporter;
class LegacyModules; class LegacyModules;
class LoggerModule; class LoggerModule;
class MainWindow; class MainWindow;
@ -90,6 +91,7 @@ namespace app {
bool m_isShell; bool m_isShell;
base::UniquePtr<MainWindow> m_mainWindow; base::UniquePtr<MainWindow> m_mainWindow;
FileList m_files; FileList m_files;
base::UniquePtr<DocumentExporter> m_exporter;
}; };
void app_refresh_screen(); void app_refresh_screen();

View File

@ -24,6 +24,7 @@
#include "base/path.h" #include "base/path.h"
#include <cstdlib>
#include <iostream> #include <iostream>
namespace app { namespace app {
@ -35,10 +36,20 @@ AppOptions::AppOptions(int argc, const char* argv[])
, m_startUI(true) , m_startUI(true)
, m_startShell(false) , m_startShell(false)
, m_verbose(false) , m_verbose(false)
, m_scale(1.0)
{ {
Option& palette = m_po.add("palette").requiresValue("GFXFILE").description("Use a specific palette by default"); Option& palette = m_po.add("palette").requiresValue("<filename>").description("Use a specific palette by default");
Option& shell = m_po.add("shell").description("Start an interactive console to execute scripts"); Option& shell = m_po.add("shell").description("Start an interactive console to execute scripts");
Option& batch = m_po.add("batch").description("Do not start the UI"); Option& batch = m_po.add("batch").description("Do not start the UI");
// Option& dataFormat = m_po.add("format").requiresValue("<name>").description("Select the format for the sprite sheet data");
Option& data = m_po.add("data").requiresValue("<filename>").description("File to store the sprite sheet metadata (.json file)");
//Option& textureFormat = m_po.add("texture-format").requiresValue("<name>").description("Output texture format.");
Option& sheet = m_po.add("sheet").requiresValue("<filename>").description("Image file to save the texture (.png)");
//Option& scale = m_po.add("scale").requiresValue("<float>").description("");
//Option& scaleMode = m_po.add("scale-mode").requiresValue("<mode>").description("Export the first given document to a JSON object");
//Option& splitLayers = m_po.add("split-layers").description("Specifies that each layer of the given file should be saved as a different image in the sheet.");
//Option& rotsprite = m_po.add("rotsprite").requiresValue("<angle1,angle2,...>").description("Specifies different angles to export the given image.");
//Option& merge = m_po.add("merge").requiresValue("<datafiles>").description("Merge several sprite sheets in one.");
Option& verbose = m_po.add("verbose").description("Explain what is being done (in stderr or a log file)"); Option& verbose = m_po.add("verbose").description("Explain what is being done (in stderr or a log file)");
Option& help = m_po.add("help").mnemonic('?').description("Display this help and exits"); Option& help = m_po.add("help").mnemonic('?').description("Display this help and exits");
Option& version = m_po.add("version").description("Output version information and exit"); Option& version = m_po.add("version").description("Output version information and exit");
@ -49,6 +60,13 @@ AppOptions::AppOptions(int argc, const char* argv[])
m_verbose = verbose.enabled(); m_verbose = verbose.enabled();
m_paletteFileName = palette.value(); m_paletteFileName = palette.value();
m_startShell = shell.enabled(); m_startShell = shell.enabled();
// m_dataFormat = dataFormat.value();
m_data = data.value();
// m_textureFormat = textureFormat.value();
m_sheet = sheet.value();
// if (scale.enabled())
// m_scale = std::strtod(scale.value().c_str(), NULL);
// m_scaleMode = scaleMode.value();
if (help.enabled()) { if (help.enabled()) {
showHelp(); showHelp();

View File

@ -41,6 +41,22 @@ public:
return m_po.values(); return m_po.values();
} }
// Export options
const std::string& dataFormat() const { return m_dataFormat; }
const std::string& data() const { return m_data; }
const std::string& textureFormat() const { return m_textureFormat; }
const std::string& sheet() const { return m_sheet; }
const double scale() const { return m_scale; }
const std::string& scaleMode() const { return m_scaleMode; }
bool hasExporterParams() {
return
!m_dataFormat.empty() ||
!m_data.empty() ||
!m_textureFormat.empty() ||
!m_sheet.empty();
}
private: private:
void showHelp(); void showHelp();
void showVersion(); void showVersion();
@ -51,6 +67,13 @@ private:
bool m_startShell; bool m_startShell;
bool m_verbose; bool m_verbose;
std::string m_paletteFileName; std::string m_paletteFileName;
std::string m_dataFormat;
std::string m_data;
std::string m_textureFormat;
std::string m_sheet;
double m_scale;
std::string m_scaleMode;
}; };
} // namespace app } // namespace app

View File

@ -0,0 +1,329 @@
/* Aseprite
* Copyright (C) 2001-2013 David Capello
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/document_exporter.h"
#include "app/document.h"
#include "app/document_api.h"
#include "app/file/file.h"
#include "base/compiler_specific.h"
#include "base/path.h"
#include "base/unique_ptr.h"
#include "gfx/size.h"
#include "raster/cel.h"
#include "raster/dithering_method.h"
#include "raster/image.h"
#include "raster/layer.h"
#include "raster/palette.h"
#include "raster/sprite.h"
#include "raster/stock.h"
#include <cstdio>
#include <fstream>
#include <iostream>
using namespace raster;
namespace app {
class DocumentExporter::Sample {
public:
Sample(Document* document, Sprite* sprite,
FrameNumber frame, const std::string& filename) :
m_document(document),
m_sprite(sprite),
m_frame(frame),
m_filename(filename) {
}
Document* document() const { return m_document; }
Sprite* sprite() const { return m_sprite; }
FrameNumber frame() const { return m_frame; }
std::string filename() const { return m_filename; }
const gfx::Size& originalSize() const { return m_originalSize; }
const gfx::Rect& trimmedBounds() const { return m_trimmedBounds; }
const gfx::Rect& inTextureBounds() const { return m_inTextureBounds; }
bool trimmed() const {
return m_trimmedBounds.x > 0
|| m_trimmedBounds.y > 0
|| m_trimmedBounds.w != m_originalSize.w
|| m_trimmedBounds.h != m_originalSize.h;
}
void setOriginalSize(const gfx::Size& size) { m_originalSize = size; }
void setTrimmedBounds(const gfx::Rect& bounds) { m_trimmedBounds = bounds; }
void setInTextureBounds(const gfx::Rect& bounds) { m_inTextureBounds = bounds; }
private:
Document* m_document;
Sprite* m_sprite;
FrameNumber m_frame;
std::string m_filename;
gfx::Size m_originalSize;
gfx::Rect m_trimmedBounds;
gfx::Rect m_inTextureBounds;
};
class DocumentExporter::Samples {
public:
typedef std::list<Sample> List;
typedef List::iterator iterator;
typedef List::const_iterator const_iterator;
void addSample(const Sample& sample) {
m_samples.push_back(sample);
}
iterator begin() { return m_samples.begin(); }
iterator end() { return m_samples.end(); }
const_iterator begin() const { return m_samples.begin(); }
const_iterator end() const { return m_samples.end(); }
private:
List m_samples;
};
class DocumentExporter::LayoutSamples {
public:
virtual ~LayoutSamples() { }
virtual void layoutSamples(Samples& samples) = 0;
};
class DocumentExporter::SimpleLayoutSamples :
public DocumentExporter::LayoutSamples {
public:
void layoutSamples(Samples& samples) OVERRIDE {
const Sprite* oldSprite = NULL;
gfx::Point framePt(0, 0);
for (Samples::iterator it=samples.begin(), end=samples.end();
it != end; ++it) {
const Sprite* sprite = it->sprite();
gfx::Size size(sprite->getWidth(), sprite->getHeight());
it->setOriginalSize(size);
it->setTrimmedBounds(gfx::Rect(gfx::Point(0, 0), size));
it->setInTextureBounds(gfx::Rect(framePt, size));
// All frames of each sprite in one row.
if (oldSprite != NULL && oldSprite != it->sprite()) {
framePt.x = 0;
framePt.y += size.h;
}
else {
framePt.x += size.w;
}
oldSprite = it->sprite();
}
}
};
void DocumentExporter::exportSheet()
{
// We output the metadata to std::cout if the user didn't specify a file.
std::ofstream fos;
std::streambuf* osbuf;
if (m_dataFilename.empty())
osbuf = std::cout.rdbuf();
else {
fos.open(m_dataFilename.c_str(), std::ios::out);
osbuf = fos.rdbuf();
}
std::ostream os(osbuf);
// Steps for sheet construction:
// 1) Capture the samples (each sprite+frame pair)
Samples samples;
captureSamples(samples);
// 2) Layout those samples in a texture field.
SimpleLayoutSamples layout;
layout.layoutSamples(samples);
// 3) Create and render the texture.
base::UniquePtr<Document> textureDocument(
createEmptyTexture(samples));
Sprite* texture = textureDocument->getSprite();
Image* textureImage = texture->getStock()->getImage(
static_cast<LayerImage*>(texture->getFolder()->getFirstLayer())
->getCel(FrameNumber(0))->getImage());
renderTexture(samples, textureImage);
// Save the metadata.
createDataFile(samples, os, textureImage);
// Save the image files.
if (!m_textureFilename.empty()) {
textureDocument->setFilename(m_textureFilename.c_str());
save_document(textureDocument.get());
}
}
void DocumentExporter::captureSamples(Samples& samples)
{
std::vector<char> buf(32);
for (std::vector<Document*>::iterator
it = m_documents.begin(),
end = m_documents.end(); it != end; ++it) {
Document* document = *it;
Sprite* sprite = document->getSprite();
for (FrameNumber frame=FrameNumber(0);
frame<sprite->getTotalFrames(); ++frame) {
base::string filename = document->getFilename();
if (sprite->getTotalFrames() > FrameNumber(1)) {
int frameNumWidth =
(sprite->getTotalFrames() < 10)? 1:
(sprite->getTotalFrames() < 100)? 2:
(sprite->getTotalFrames() < 1000)? 3: 4;
std::sprintf(&buf[0], "%0*d", frameNumWidth, frame);
base::string path = base::get_file_path(filename);
base::string title = base::get_file_title(filename);
base::string ext = base::get_file_extension(filename);
filename = base::join_path(path, title + &buf[0] + "." + ext);
}
samples.addSample(Sample(document, sprite, frame, filename));
}
}
}
Document* DocumentExporter::createEmptyTexture(const Samples& samples)
{
Palette* palette = NULL;
PixelFormat pixelFormat = IMAGE_INDEXED;
gfx::Rect fullTextureBounds;
int maxColors = 256;
for (Samples::const_iterator
it = samples.begin(),
end = samples.end(); it != end; ++it) {
// We try to render an indexed image. But if we find a sprite with
// two or more palettes, or two of the sprites have different
// palettes, we've to use RGB format.
if (pixelFormat == IMAGE_INDEXED) {
if (it->sprite()->getPixelFormat() != IMAGE_INDEXED) {
pixelFormat = IMAGE_RGB;
}
else if (it->sprite()->getPalettes().size() > 1) {
pixelFormat = IMAGE_RGB;
}
else if (palette != NULL
&& palette->countDiff(it->sprite()->getPalette(FrameNumber(0)), NULL, NULL) > 0) {
pixelFormat = IMAGE_RGB;
}
else
palette = it->sprite()->getPalette(FrameNumber(0));
}
fullTextureBounds = fullTextureBounds.createUnion(it->inTextureBounds());
}
base::UniquePtr<Document> document(Document::createBasicDocument(pixelFormat,
fullTextureBounds.w, fullTextureBounds.h, maxColors));
if (palette != NULL)
document->getSprite()->setPalette(palette, false);
return document.release();
}
void DocumentExporter::renderTexture(const Samples& samples, Image* textureImage)
{
textureImage->clear(0);
for (Samples::const_iterator
it = samples.begin(),
end = samples.end(); it != end; ++it) {
// Make the sprite compatible with the texture so the render()
// works correctly.
if (it->sprite()->getPixelFormat() != textureImage->getPixelFormat()) {
DocumentApi docApi(it->document(), NULL); // DocumentApi without undo
docApi.setPixelFormat(it->sprite(), textureImage->getPixelFormat(),
DITHERING_NONE);
}
it->sprite()->render(textureImage,
it->inTextureBounds().x - it->trimmedBounds().x,
it->inTextureBounds().y - it->trimmedBounds().y,
it->frame());
}
}
void DocumentExporter::createDataFile(const Samples& samples, std::ostream& os, Image* textureImage)
{
os << "{ \"frames\": {\n";
for (Samples::const_iterator
it = samples.begin(),
end = samples.end(); it != end; ) {
gfx::Size srcSize = it->originalSize();
gfx::Rect spriteSourceBounds = it->trimmedBounds();
gfx::Rect frameBounds = it->inTextureBounds();
os << " \"" << it->filename() << "\": {\n"
<< " \"frame\": { "
<< "\"x\": " << frameBounds.x << ", "
<< "\"y\": " << frameBounds.y << ", "
<< "\"w\": " << frameBounds.w << ", "
<< "\"h\": " << frameBounds.h << " },\n"
<< " \"rotated\": false,\n"
<< " \"trimmed\": " << (it->trimmed() ? "true": "false") << ",\n"
<< " \"spriteSourceSize\": { "
<< "\"x\": " << spriteSourceBounds.x << ", "
<< "\"y\": " << spriteSourceBounds.y << ", "
<< "\"w\": " << spriteSourceBounds.w << ", "
<< "\"h\": " << spriteSourceBounds.h << " },\n"
<< " \"sourceSize\": { "
<< "\"w\": " << srcSize.w << ", "
<< "\"h\": " << srcSize.h << " },\n"
<< " \"duration\": " << it->sprite()->getFrameDuration(it->frame()) << "\n"
<< " }";
if (++it != samples.end())
os << ",\n";
else
os << "\n";
}
os << " },\n"
<< " \"meta\": {\n"
<< " \"app\": \"" << WEBSITE << "\",\n"
<< " \"version\": \"" << VERSION << "\",\n";
if (!m_textureFilename.empty())
os << " \"image\": \"" << m_textureFilename.c_str() << "\",\n";
os << " \"format\": \"" << (textureImage->getPixelFormat() == IMAGE_RGB ? "RGBA8888": "I8") << "\",\n"
<< " \"size\": { "
<< "\"w\": " << textureImage->getWidth() << ", "
<< "\"h\": " << textureImage->getHeight() << " },\n"
<< " \"scale\": \"" << m_scale << "\"\n"
<< " }\n"
<< "}\n";
}
} // namespace app

111
src/app/document_exporter.h Normal file
View File

@ -0,0 +1,111 @@
/* Aseprite
* Copyright (C) 2001-2013 David Capello
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef APP_DOCUMENT_EXPORTER_H_INCLUDED
#define APP_DOCUMENT_EXPORTER_H_INCLUDED
#include "base/disable_copying.h"
#include "gfx/fwd.h"
#include <iosfwd>
#include <vector>
namespace raster {
class Image;
}
namespace app {
class Document;
class DocumentExporter {
public:
enum DataFormat {
JsonDataFormat,
DefaultDataFormat = JsonDataFormat
};
enum TextureFormat {
JsonTextureFormat,
DefaultTextureFormat = JsonTextureFormat
};
enum ScaleMode {
DefaultScaleMode
};
DocumentExporter() :
m_dataFormat(DefaultDataFormat),
m_textureFormat(DefaultTextureFormat),
m_scaleMode(DefaultScaleMode) {
}
void setDataFormat(DataFormat format) {
m_dataFormat = format;
}
void setDataFilename(const std::string& filename) {
m_dataFilename = filename;
}
void setTextureFormat(TextureFormat format) {
m_textureFormat = format;
}
void setTextureFilename(const std::string& filename) {
m_textureFilename = filename;
}
void setScale(double scale) {
m_scale = scale;
}
void setScaleMode(ScaleMode mode) {
m_scaleMode = mode;
}
void addDocument(Document* document) {
m_documents.push_back(document);
}
void exportSheet();
private:
class Sample;
class Samples;
class LayoutSamples;
class SimpleLayoutSamples;
void captureSamples(Samples& samples);
Document* createEmptyTexture(const Samples& samples);
void renderTexture(const Samples& samples, raster::Image* textureImage);
void createDataFile(const Samples& samples, std::ostream& os, raster::Image* textureImage);
DataFormat m_dataFormat;
std::string m_dataFilename;
TextureFormat m_textureFormat;
std::string m_textureFilename;
double m_scale;
ScaleMode m_scaleMode;
std::vector<Document*> m_documents;
DISABLE_COPYING(DocumentExporter);
};
} // namespace app
#endif

View File

@ -134,6 +134,10 @@ void UIContext::onAddDocument(Document* document)
// base method // base method
Context::onAddDocument(document); Context::onAddDocument(document);
// We don't create views in batch mode.
if (!App::instance()->isGui())
return;
// Add a new view for this document // Add a new view for this document
DocumentView* view = new DocumentView(document, DocumentView::Normal); DocumentView* view = new DocumentView(document, DocumentView::Normal);