1 // Aseprite
2 // Copyright (C) 2001-2018  David Capello
3 //
4 // This program is distributed under the terms of
5 // the End-User License Agreement for Aseprite.
6 
7 #ifdef HAVE_CONFIG_H
8 #include "config.h"
9 #endif
10 
11 #include "app/doc_exporter.h"
12 
13 #include "app/cmd/set_pixel_format.h"
14 #include "app/console.h"
15 #include "app/context.h"
16 #include "app/doc.h"
17 #include "app/file/file.h"
18 #include "app/filename_formatter.h"
19 #include "app/restore_visible_layers.h"
20 #include "base/convert_to.h"
21 #include "base/fs.h"
22 #include "base/fstream_path.h"
23 #include "base/replace_string.h"
24 #include "base/shared_ptr.h"
25 #include "base/string.h"
26 #include "base/unique_ptr.h"
27 #include "doc/algorithm/shrink_bounds.h"
28 #include "doc/cel.h"
29 #include "doc/frame_tag.h"
30 #include "doc/image.h"
31 #include "doc/layer.h"
32 #include "doc/palette.h"
33 #include "doc/primitives.h"
34 #include "doc/selected_frames.h"
35 #include "doc/selected_layers.h"
36 #include "doc/slice.h"
37 #include "doc/sprite.h"
38 #include "gfx/packing_rects.h"
39 #include "gfx/size.h"
40 #include "render/dithering_algorithm.h"
41 #include "render/ordered_dither.h"
42 #include "render/render.h"
43 
44 #include <cstdio>
45 #include <fstream>
46 #include <iomanip>
47 #include <iostream>
48 #include <list>
49 
50 using namespace doc;
51 
52 namespace {
53 
escape_for_json(const std::string & path)54 std::string escape_for_json(const std::string& path)
55 {
56   std::string res = path;
57   base::replace_string(res, "\\", "\\\\");
58   base::replace_string(res, "\"", "\\\"");
59   return res;
60 }
61 
operator <<(std::ostream & os,const doc::UserData & data)62 std::ostream& operator<<(std::ostream& os, const doc::UserData& data)
63 {
64   doc::color_t color = data.color();
65   if (doc::rgba_geta(color)) {
66     os << ", \"color\": \"#"
67        << std::hex << std::setfill('0')
68        << std::setw(2) << (int)doc::rgba_getr(color)
69        << std::setw(2) << (int)doc::rgba_getg(color)
70        << std::setw(2) << (int)doc::rgba_getb(color)
71        << std::setw(2) << (int)doc::rgba_geta(color)
72        << std::dec
73        << "\"";
74   }
75   if (!data.text().empty())
76     os << ", \"data\": \"" << escape_for_json(data.text()) << "\"";
77   return os;
78 }
79 
80 } // anonymous namespace
81 
82 namespace app {
83 
84 class SampleBounds {
85 public:
SampleBounds(Sprite * sprite)86   SampleBounds(Sprite* sprite) :
87     m_originalSize(sprite->width(), sprite->height()),
88     m_trimmedBounds(0, 0, sprite->width(), sprite->height()),
89     m_inTextureBounds(0, 0, sprite->width(), sprite->height()) {
90   }
91 
trimmed() const92   bool trimmed() const {
93     return m_trimmedBounds.x > 0
94       || m_trimmedBounds.y > 0
95       || m_trimmedBounds.w != m_originalSize.w
96       || m_trimmedBounds.h != m_originalSize.h;
97   }
98 
originalSize() const99   const gfx::Size& originalSize() const { return m_originalSize; }
trimmedBounds() const100   const gfx::Rect& trimmedBounds() const { return m_trimmedBounds; }
inTextureBounds() const101   const gfx::Rect& inTextureBounds() const { return m_inTextureBounds; }
102 
setTrimmedBounds(const gfx::Rect & bounds)103   void setTrimmedBounds(const gfx::Rect& bounds) { m_trimmedBounds = bounds; }
setInTextureBounds(const gfx::Rect & bounds)104   void setInTextureBounds(const gfx::Rect& bounds) { m_inTextureBounds = bounds; }
105 
106 private:
107   gfx::Size m_originalSize;
108   gfx::Rect m_trimmedBounds;
109   gfx::Rect m_inTextureBounds;
110 };
111 
112 typedef base::SharedPtr<SampleBounds> SampleBoundsPtr;
113 
Item(Doc * doc,doc::FrameTag * frameTag,doc::SelectedLayers * selLayers,doc::SelectedFrames * selFrames)114 DocExporter::Item::Item(Doc* doc,
115                              doc::FrameTag* frameTag,
116                              doc::SelectedLayers* selLayers,
117                              doc::SelectedFrames* selFrames)
118   : doc(doc)
119   , frameTag(frameTag)
120   , selLayers(selLayers ? new doc::SelectedLayers(*selLayers): nullptr)
121   , selFrames(selFrames ? new doc::SelectedFrames(*selFrames): nullptr)
122 {
123 }
124 
Item(Item && other)125 DocExporter::Item::Item(Item&& other)
126   : doc(other.doc)
127   , frameTag(other.frameTag)
128   , selLayers(other.selLayers)
129   , selFrames(other.selFrames)
130 {
131   other.selLayers = nullptr;
132   other.selFrames = nullptr;
133 }
134 
~Item()135 DocExporter::Item::~Item()
136 {
137   delete selLayers;
138   delete selFrames;
139 }
140 
frames() const141 int DocExporter::Item::frames() const
142 {
143   if (selFrames)
144     return selFrames->size();
145   else if (frameTag) {
146     int result = frameTag->toFrame() - frameTag->fromFrame() + 1;
147     return MID(1, result, doc->sprite()->totalFrames());
148   }
149   else
150     return doc->sprite()->totalFrames();
151 }
152 
firstFrame() const153 doc::frame_t DocExporter::Item::firstFrame() const
154 {
155   if (selFrames)
156     return selFrames->firstFrame();
157   else if (frameTag)
158     return frameTag->fromFrame();
159   else
160     return 0;
161 }
162 
getSelectedFrames() const163 doc::SelectedFrames DocExporter::Item::getSelectedFrames() const
164 {
165   if (selFrames)
166     return *selFrames;
167 
168   doc::SelectedFrames frames;
169   if (frameTag) {
170     frames.insert(MID(0, frameTag->fromFrame(), doc->sprite()->lastFrame()),
171                   MID(0, frameTag->toFrame(), doc->sprite()->lastFrame()));
172   }
173   else {
174     frames.insert(0, doc->sprite()->lastFrame());
175   }
176   return frames;
177 }
178 
179 class DocExporter::Sample {
180 public:
Sample(Doc * document,Sprite * sprite,SelectedLayers * selLayers,frame_t frame,const std::string & filename,int innerPadding)181   Sample(Doc* document, Sprite* sprite, SelectedLayers* selLayers,
182          frame_t frame, const std::string& filename, int innerPadding) :
183     m_document(document),
184     m_sprite(sprite),
185     m_selLayers(selLayers),
186     m_frame(frame),
187     m_filename(filename),
188     m_innerPadding(innerPadding),
189     m_bounds(new SampleBounds(sprite)),
190     m_isDuplicated(false) {
191   }
192 
document() const193   Doc* document() const { return m_document; }
sprite() const194   Sprite* sprite() const { return m_sprite; }
layer() const195   Layer* layer() const {
196     return (m_selLayers && m_selLayers->size() == 1 ? *m_selLayers->begin():
197                                                       nullptr);
198   }
selectedLayers() const199   SelectedLayers* selectedLayers() const { return m_selLayers; }
frame() const200   frame_t frame() const { return m_frame; }
filename() const201   std::string filename() const { return m_filename; }
originalSize() const202   const gfx::Size& originalSize() const { return m_bounds->originalSize(); }
trimmedBounds() const203   const gfx::Rect& trimmedBounds() const { return m_bounds->trimmedBounds(); }
inTextureBounds() const204   const gfx::Rect& inTextureBounds() const { return m_bounds->inTextureBounds(); }
205 
requiredSize() const206   gfx::Size requiredSize() const {
207     gfx::Size size = m_bounds->trimmedBounds().size();
208     size.w += 2*m_innerPadding;
209     size.h += 2*m_innerPadding;
210     return size;
211   }
212 
trimmed() const213   bool trimmed() const {
214     return m_bounds->trimmed();
215   }
216 
setTrimmedBounds(const gfx::Rect & bounds)217   void setTrimmedBounds(const gfx::Rect& bounds) { m_bounds->setTrimmedBounds(bounds); }
setInTextureBounds(const gfx::Rect & bounds)218   void setInTextureBounds(const gfx::Rect& bounds) { m_bounds->setInTextureBounds(bounds); }
219 
isDuplicated() const220   bool isDuplicated() const { return m_isDuplicated; }
isEmpty() const221   bool isEmpty() const { return m_bounds->trimmedBounds().isEmpty(); }
sharedBounds() const222   SampleBoundsPtr sharedBounds() const { return m_bounds; }
223 
setSharedBounds(const SampleBoundsPtr & bounds)224   void setSharedBounds(const SampleBoundsPtr& bounds) {
225     m_isDuplicated = true;
226     m_bounds = bounds;
227   }
228 
229 private:
230   Doc* m_document;
231   Sprite* m_sprite;
232   SelectedLayers* m_selLayers;
233   frame_t m_frame;
234   std::string m_filename;
235   int m_borderPadding;
236   int m_shapePadding;
237   int m_innerPadding;
238   SampleBoundsPtr m_bounds;
239   bool m_isDuplicated;
240 };
241 
242 class DocExporter::Samples {
243 public:
244   typedef std::list<Sample> List;
245   typedef List::iterator iterator;
246   typedef List::const_iterator const_iterator;
247 
empty() const248   bool empty() const { return m_samples.empty(); }
249 
addSample(const Sample & sample)250   void addSample(const Sample& sample) {
251     m_samples.push_back(sample);
252   }
253 
begin()254   iterator begin() { return m_samples.begin(); }
end()255   iterator end() { return m_samples.end(); }
begin() const256   const_iterator begin() const { return m_samples.begin(); }
end() const257   const_iterator end() const { return m_samples.end(); }
258 
259 private:
260   List m_samples;
261 };
262 
263 class DocExporter::LayoutSamples {
264 public:
~LayoutSamples()265   virtual ~LayoutSamples() { }
266   virtual void layoutSamples(Samples& samples, int borderPadding, int shapePadding, int& width, int& height) = 0;
267 };
268 
269 class DocExporter::SimpleLayoutSamples :
270     public DocExporter::LayoutSamples {
271 public:
SimpleLayoutSamples(SpriteSheetType type)272   SimpleLayoutSamples(SpriteSheetType type)
273     : m_type(type) {
274   }
275 
layoutSamples(Samples & samples,int borderPadding,int shapePadding,int & width,int & height)276   void layoutSamples(Samples& samples, int borderPadding, int shapePadding, int& width, int& height) override {
277     const Sprite* oldSprite = NULL;
278     const Layer* oldLayer = NULL;
279 
280     gfx::Point framePt(borderPadding, borderPadding);
281     gfx::Size rowSize(0, 0);
282 
283     for (auto& sample : samples) {
284       if (sample.isDuplicated())
285         continue;
286 
287       if (sample.isEmpty()) {
288         sample.setInTextureBounds(gfx::Rect(0, 0, 0, 0));
289         continue;
290       }
291 
292       const Sprite* sprite = sample.sprite();
293       const Layer* layer = sample.layer();
294       gfx::Size size = sample.requiredSize();
295 
296       if (oldSprite) {
297         if (m_type == SpriteSheetType::Columns) {
298           // If the user didn't specify a height for the texture, we
299           // put each sprite/layer in a different column.
300           if (height == 0) {
301             // New sprite or layer, go to next column.
302             if (oldSprite != sprite || oldLayer != layer) {
303               framePt.x += rowSize.w + shapePadding;
304               framePt.y = borderPadding;
305               rowSize = size;
306             }
307           }
308           // When a texture height is specified, we can put different
309           // sprites/layers in each column until we reach the texture
310           // bottom-border.
311           else if (framePt.y+size.h > height-borderPadding) {
312             framePt.x += rowSize.w + shapePadding;
313             framePt.y = borderPadding;
314             rowSize = size;
315           }
316         }
317         else {
318           // If the user didn't specify a width for the texture, we put
319           // each sprite/layer in a different row.
320           if (width == 0) {
321             // New sprite or layer, go to next row.
322             if (oldSprite != sprite || oldLayer != layer) {
323               framePt.x = borderPadding;
324               framePt.y += rowSize.h + shapePadding;
325               rowSize = size;
326             }
327           }
328           // When a texture width is specified, we can put different
329           // sprites/layers in each row until we reach the texture
330           // right-border.
331           else if (framePt.x+size.w > width-borderPadding) {
332             framePt.x = borderPadding;
333             framePt.y += rowSize.h + shapePadding;
334             rowSize = size;
335           }
336         }
337       }
338 
339       sample.setInTextureBounds(gfx::Rect(framePt, size));
340 
341       // Next frame position.
342       if (m_type == SpriteSheetType::Columns) {
343         framePt.y += size.h + shapePadding;
344       }
345       else {
346         framePt.x += size.w + shapePadding;
347       }
348 
349       rowSize = rowSize.createUnion(size);
350 
351       oldSprite = sprite;
352       oldLayer = layer;
353     }
354   }
355 
356 private:
357   SpriteSheetType m_type;
358 };
359 
360 class DocExporter::BestFitLayoutSamples :
361     public DocExporter::LayoutSamples {
362 public:
layoutSamples(Samples & samples,int borderPadding,int shapePadding,int & width,int & height)363   void layoutSamples(Samples& samples, int borderPadding, int shapePadding, int& width, int& height) override {
364     gfx::PackingRects pr;
365 
366     // TODO Add support for shape paddings
367 
368     for (auto& sample : samples) {
369       if (sample.isDuplicated() ||
370           sample.isEmpty())
371         continue;
372 
373       pr.add(sample.requiredSize());
374     }
375 
376     if (width == 0 || height == 0) {
377       gfx::Size sz = pr.bestFit();
378       width = sz.w;
379       height = sz.h;
380     }
381     else
382       pr.pack(gfx::Size(width, height));
383 
384     auto it = samples.begin();
385     for (auto& rc : pr) {
386       if (it->isDuplicated())
387         continue;
388 
389       ASSERT(it != samples.end());
390       it->setInTextureBounds(rc);
391       ++it;
392     }
393   }
394 };
395 
DocExporter()396 DocExporter::DocExporter()
397  : m_dataFormat(DefaultDataFormat)
398  , m_textureWidth(0)
399  , m_textureHeight(0)
400  , m_sheetType(SpriteSheetType::None)
401  , m_ignoreEmptyCels(false)
402  , m_borderPadding(0)
403  , m_shapePadding(0)
404  , m_innerPadding(0)
405  , m_trimCels(false)
406  , m_listFrameTags(false)
407  , m_listLayers(false)
408  , m_listSlices(false)
409 {
410 }
411 
exportSheet(Context * ctx)412 Doc* DocExporter::exportSheet(Context* ctx)
413 {
414   // We output the metadata to std::cout if the user didn't specify a file.
415   std::ofstream fos;
416   std::streambuf* osbuf = nullptr;
417   if (m_dataFilename.empty()) {
418     // Redirect to stdout if we are running in batch mode
419     if (!ctx->isUIAvailable())
420       osbuf = std::cout.rdbuf();
421   }
422   else {
423     fos.open(FSTREAM_PATH(m_dataFilename), std::ios::out);
424     osbuf = fos.rdbuf();
425   }
426   std::ostream os(osbuf);
427 
428   // Steps for sheet construction:
429   // 1) Capture the samples (each sprite+frame pair)
430   Samples samples;
431   captureSamples(samples);
432   if (samples.empty()) {
433     Console console;
434     console.printf("No documents to export");
435     return nullptr;
436   }
437 
438   // 2) Layout those samples in a texture field.
439   layoutSamples(samples);
440 
441   // 3) Create and render the texture.
442   base::UniquePtr<Doc> textureDocument(
443     createEmptyTexture(samples));
444 
445   Sprite* texture = textureDocument->sprite();
446   Image* textureImage = texture->root()->firstLayer()
447     ->cel(frame_t(0))->image();
448 
449   renderTexture(ctx, samples, textureImage);
450 
451   // Save the metadata.
452   if (osbuf)
453     createDataFile(samples, os, textureImage);
454 
455   // Save the image files.
456   if (!m_textureFilename.empty()) {
457     textureDocument->setFilename(m_textureFilename.c_str());
458     int ret = save_document(ctx, textureDocument.get());
459     if (ret == 0)
460       textureDocument->markAsSaved();
461   }
462 
463   return textureDocument.release();
464 }
465 
calculateSheetSize()466 gfx::Size DocExporter::calculateSheetSize()
467 {
468   Samples samples;
469   captureSamples(samples);
470   layoutSamples(samples);
471   return calculateSheetSize(samples);
472 }
473 
captureSamples(Samples & samples)474 void DocExporter::captureSamples(Samples& samples)
475 {
476   for (auto& item : m_documents) {
477     Doc* doc = item.doc;
478     Sprite* sprite = doc->sprite();
479     Layer* layer = (item.selLayers && item.selLayers->size() == 1 ?
480                     *item.selLayers->begin(): nullptr);
481     FrameTag* frameTag = item.frameTag;
482     int frames = item.frames();
483 
484     std::string format = m_filenameFormat;
485     if (format.empty()) {
486       format = get_default_filename_format_for_sheet(
487         doc->filename(),
488         (frames > 1),                   // Has frames
489         (layer != nullptr),             // Has layer
490         (frameTag != nullptr));         // Has frame tag
491     }
492 
493     frame_t frameFirst = item.firstFrame();
494     for (frame_t frame : item.getSelectedFrames()) {
495       FrameTag* innerTag = (frameTag ? frameTag: sprite->frameTags().innerTag(frame));
496       FrameTag* outerTag = sprite->frameTags().outerTag(frame);
497       FilenameInfo fnInfo;
498       fnInfo
499         .filename(doc->filename())
500         .layerName(layer ? layer->name(): "")
501         .groupName(layer && layer->parent() != sprite->root() ? layer->parent()->name(): "")
502         .innerTagName(innerTag ? innerTag->name(): "")
503         .outerTagName(outerTag ? outerTag->name(): "")
504         .frame((frames > 1) ? frame-frameFirst: frame_t(-1));
505 
506       std::string filename = filename_formatter(format, fnInfo);
507 
508       Sample sample(doc, sprite, item.selLayers, frame, filename, m_innerPadding);
509       Cel* cel = nullptr;
510       Cel* link = nullptr;
511       bool done = false;
512 
513       if (layer && layer->isImage()) {
514         cel = layer->cel(frame);
515         if (cel)
516           link = cel->link();
517       }
518 
519       // Re-use linked samples
520       if (link) {
521         for (const Sample& other : samples) {
522           if (other.sprite() == sprite &&
523               other.layer() == layer &&
524               other.frame() == link->frame()) {
525             ASSERT(!other.isDuplicated());
526 
527             sample.setSharedBounds(other.sharedBounds());
528             done = true;
529             break;
530           }
531         }
532         // "done" variable can be false here, e.g. when we export a
533         // frame tag and the first linked cel is outside the tag range.
534         ASSERT(done || (!done && frameTag));
535       }
536 
537       if (!done && (m_ignoreEmptyCels || m_trimCels)) {
538         // Ignore empty cels
539         if (layer && layer->isImage() && !cel)
540           continue;
541 
542         base::UniquePtr<Image> sampleRender(
543           Image::create(sprite->pixelFormat(),
544             sprite->width(),
545             sprite->height(),
546             m_sampleRenderBuf));
547 
548         sampleRender->setMaskColor(sprite->transparentColor());
549         clear_image(sampleRender, sprite->transparentColor());
550         renderSample(sample, sampleRender, 0, 0);
551 
552         gfx::Rect frameBounds;
553         doc::color_t refColor = 0;
554 
555         if (m_trimCels) {
556           if ((layer &&
557                layer->isBackground()) ||
558               (!layer &&
559                sprite->backgroundLayer() &&
560                sprite->backgroundLayer()->isVisible())) {
561             refColor = get_pixel(sampleRender, 0, 0);
562           }
563           else {
564             refColor = sprite->transparentColor();
565           }
566         }
567         else if (m_ignoreEmptyCels)
568           refColor = sprite->transparentColor();
569 
570         if (!algorithm::shrink_bounds(sampleRender, frameBounds, refColor)) {
571           // If shrink_bounds() returns false, it's because the whole
572           // image is transparent (equal to the mask color).
573 
574           // Should we ignore this empty frame? (i.e. don't include
575           // the frame in the sprite sheet)
576           if (m_ignoreEmptyCels) {
577             for (FrameTag* tag : sprite->frameTags()) {
578               auto& delta = m_tagDelta[tag->id()];
579 
580               if (frame < tag->fromFrame()) --delta.first;
581               if (frame <= tag->toFrame()) --delta.second;
582             }
583             continue;
584           }
585 
586           // Create an empty entry for this completely trimmed frame
587           // anyway to get its duration in the list of frames.
588           sample.setTrimmedBounds(frameBounds = gfx::Rect(0, 0, 0, 0));
589         }
590 
591         if (m_trimCels)
592           sample.setTrimmedBounds(frameBounds);
593       }
594 
595       samples.addSample(sample);
596     }
597   }
598 }
599 
layoutSamples(Samples & samples)600 void DocExporter::layoutSamples(Samples& samples)
601 {
602   switch (m_sheetType) {
603     case SpriteSheetType::Packed: {
604       BestFitLayoutSamples layout;
605       layout.layoutSamples(
606         samples, m_borderPadding, m_shapePadding,
607         m_textureWidth, m_textureHeight);
608       break;
609     }
610     default: {
611       SimpleLayoutSamples layout(m_sheetType);
612       layout.layoutSamples(
613         samples, m_borderPadding, m_shapePadding,
614         m_textureWidth, m_textureHeight);
615       break;
616     }
617   }
618 }
619 
calculateSheetSize(const Samples & samples) const620 gfx::Size DocExporter::calculateSheetSize(const Samples& samples) const
621 {
622   gfx::Rect fullTextureBounds(0, 0, m_textureWidth, m_textureHeight);
623 
624   for (const auto& sample : samples) {
625     if (sample.isDuplicated() ||
626         sample.isEmpty())
627       continue;
628 
629     gfx::Rect sampleBounds = sample.inTextureBounds();
630 
631     // If the user specified a fixed sprite sheet size, we add the
632     // border padding in the sample size to do an union between
633     // fullTextureBounds and sample's inTextureBounds (generally, it
634     // shouldn't make fullTextureBounds bigger).
635     if (m_textureWidth > 0) sampleBounds.w += m_borderPadding;
636     if (m_textureHeight > 0) sampleBounds.h += m_borderPadding;
637 
638     fullTextureBounds |= sampleBounds;
639   }
640 
641   // If the user didn't specified the sprite sheet size, the border is
642   // added right here (the left/top border padding should be added by
643   // the DocExporter::LayoutSamples() impl).
644   if (m_textureWidth == 0) fullTextureBounds.w += m_borderPadding;
645   if (m_textureHeight == 0) fullTextureBounds.h += m_borderPadding;
646 
647   return gfx::Size(fullTextureBounds.x+fullTextureBounds.w,
648                    fullTextureBounds.y+fullTextureBounds.h);
649 }
650 
createEmptyTexture(const Samples & samples) const651 Doc* DocExporter::createEmptyTexture(const Samples& samples) const
652 {
653   PixelFormat pixelFormat = IMAGE_INDEXED;
654   Palette* palette = nullptr;
655   int maxColors = 256;
656 
657   for (const auto& sample : samples) {
658     if (sample.isDuplicated() ||
659         sample.isEmpty())
660       continue;
661 
662     // We try to render an indexed image. But if we find a sprite with
663     // two or more palettes, or two of the sprites have different
664     // palettes, we've to use RGB format.
665     if (pixelFormat == IMAGE_INDEXED) {
666       if (sample.sprite()->pixelFormat() != IMAGE_INDEXED) {
667         pixelFormat = IMAGE_RGB;
668       }
669       else if (sample.sprite()->getPalettes().size() > 1) {
670         pixelFormat = IMAGE_RGB;
671       }
672       else if (palette != NULL
673         && palette->countDiff(sample.sprite()->palette(frame_t(0)), NULL, NULL) > 0) {
674         pixelFormat = IMAGE_RGB;
675       }
676       else
677         palette = sample.sprite()->palette(frame_t(0));
678     }
679   }
680 
681   gfx::Size textureSize = calculateSheetSize(samples);
682 
683   base::UniquePtr<Sprite> sprite(
684     Sprite::createBasicSprite(
685       pixelFormat, textureSize.w, textureSize.h, maxColors));
686 
687   if (palette != NULL)
688     sprite->setPalette(palette, false);
689 
690   base::UniquePtr<Doc> document(new Doc(sprite));
691   sprite.release();
692 
693   return document.release();
694 }
695 
renderTexture(Context * ctx,const Samples & samples,Image * textureImage) const696 void DocExporter::renderTexture(Context* ctx, const Samples& samples, Image* textureImage) const
697 {
698   textureImage->clear(0);
699 
700   for (const auto& sample : samples) {
701     if (sample.isDuplicated() ||
702         sample.isEmpty())
703       continue;
704 
705     // Make the sprite compatible with the texture so the render()
706     // works correctly.
707     if (sample.sprite()->pixelFormat() != textureImage->pixelFormat()) {
708       cmd::SetPixelFormat(
709         sample.sprite(),
710         textureImage->pixelFormat(),
711         render::DitheringAlgorithm::None,
712         render::DitheringMatrix(),
713         nullptr)                // TODO add a delegate to show progress
714         .execute(ctx);
715     }
716 
717     renderSample(sample, textureImage,
718       sample.inTextureBounds().x+m_innerPadding,
719       sample.inTextureBounds().y+m_innerPadding);
720   }
721 }
722 
createDataFile(const Samples & samples,std::ostream & os,Image * textureImage)723 void DocExporter::createDataFile(const Samples& samples, std::ostream& os, Image* textureImage)
724 {
725   std::string frames_begin;
726   std::string frames_end;
727   bool filename_as_key = false;
728   bool filename_as_attr = false;
729 
730   // TODO we should use some string templates system here
731   switch (m_dataFormat) {
732     case JsonHashDataFormat:
733       frames_begin = "{";
734       frames_end = "}";
735       filename_as_key = true;
736       filename_as_attr = false;
737       break;
738     case JsonArrayDataFormat:
739       frames_begin = "[";
740       frames_end = "]";
741       filename_as_key = false;
742       filename_as_attr = true;
743       break;
744   }
745 
746   os << "{ \"frames\": " << frames_begin << "\n";
747   for (Samples::const_iterator
748          it = samples.begin(),
749          end = samples.end(); it != end; ) {
750     const Sample& sample = *it;
751     gfx::Size srcSize = sample.originalSize();
752     gfx::Rect spriteSourceBounds = sample.trimmedBounds();
753     gfx::Rect frameBounds = sample.inTextureBounds();
754 
755     if (filename_as_key)
756       os << "   \"" << escape_for_json(sample.filename()) << "\": {\n";
757     else if (filename_as_attr)
758       os << "   {\n"
759          << "    \"filename\": \"" << escape_for_json(sample.filename()) << "\",\n";
760 
761     os << "    \"frame\": { "
762        << "\"x\": " << frameBounds.x << ", "
763        << "\"y\": " << frameBounds.y << ", "
764        << "\"w\": " << frameBounds.w << ", "
765        << "\"h\": " << frameBounds.h << " },\n"
766        << "    \"rotated\": false,\n"
767        << "    \"trimmed\": " << (sample.trimmed() ? "true": "false") << ",\n"
768        << "    \"spriteSourceSize\": { "
769        << "\"x\": " << spriteSourceBounds.x << ", "
770        << "\"y\": " << spriteSourceBounds.y << ", "
771        << "\"w\": " << spriteSourceBounds.w << ", "
772        << "\"h\": " << spriteSourceBounds.h << " },\n"
773        << "    \"sourceSize\": { "
774        << "\"w\": " << srcSize.w << ", "
775        << "\"h\": " << srcSize.h << " },\n"
776        << "    \"duration\": " << sample.sprite()->frameDuration(sample.frame()) << "\n"
777        << "   }";
778 
779     if (++it != samples.end())
780       os << ",\n";
781     else
782       os << "\n";
783   }
784   os << " " << frames_end;
785 
786   // "meta" property
787   os << ",\n"
788      << " \"meta\": {\n"
789      << "  \"app\": \"" << WEBSITE << "\",\n"
790      << "  \"version\": \"" << VERSION << "\",\n";
791 
792   if (!m_textureFilename.empty())
793     os << "  \"image\": \"" << escape_for_json(m_textureFilename).c_str() << "\",\n";
794 
795   os << "  \"format\": \"" << (textureImage->pixelFormat() == IMAGE_RGB ? "RGBA8888": "I8") << "\",\n"
796      << "  \"size\": { "
797      << "\"w\": " << textureImage->width() << ", "
798      << "\"h\": " << textureImage->height() << " },\n"
799      << "  \"scale\": \"1\"";
800 
801   // meta.frameTags
802   if (m_listFrameTags) {
803     os << ",\n"
804        << "  \"frameTags\": [";
805 
806     bool firstTag = true;
807     for (auto& item : m_documents) {
808       Doc* doc = item.doc;
809       Sprite* sprite = doc->sprite();
810 
811       for (FrameTag* tag : sprite->frameTags()) {
812         if (firstTag)
813           firstTag = false;
814         else
815           os << ",";
816 
817         std::pair<int, int> delta(0, 0);
818         if (!m_tagDelta.empty())
819           delta = m_tagDelta[tag->id()];
820 
821         os << "\n   { \"name\": \"" << escape_for_json(tag->name()) << "\","
822            << " \"from\": " << (tag->fromFrame()+delta.first) << ","
823            << " \"to\": " << (tag->toFrame()+delta.second) << ","
824            << " \"direction\": \"" << escape_for_json(convert_anidir_to_string(tag->aniDir())) << "\" }";
825       }
826     }
827     os << "\n  ]";
828   }
829 
830   // meta.layers
831   if (m_listLayers) {
832     os << ",\n"
833        << "  \"layers\": [";
834 
835     bool firstLayer = true;
836     for (auto& item : m_documents) {
837       Doc* doc = item.doc;
838       Sprite* sprite = doc->sprite();
839       LayerList layers;
840 
841       if (item.selLayers)
842         layers = item.selLayers->toLayerList();
843       else
844         layers = sprite->allVisibleLayers();
845 
846       for (Layer* layer : layers) {
847         if (firstLayer)
848           firstLayer = false;
849         else
850           os << ",";
851         os << "\n   { \"name\": \"" << escape_for_json(layer->name()) << "\"";
852 
853         if (layer->parent() != layer->sprite()->root())
854           os << ", \"group\": \"" << escape_for_json(layer->parent()->name()) << "\"";
855 
856         if (LayerImage* layerImg = dynamic_cast<LayerImage*>(layer)) {
857           os << ", \"opacity\": " << layerImg->opacity()
858              << ", \"blendMode\": \"" << blend_mode_to_string(layerImg->blendMode()) << "\"";
859         }
860         os << layer->userData();
861 
862         // Cels
863         CelList cels;
864         layer->getCels(cels);
865         bool someCelWithData = false;
866         for (const Cel* cel : cels) {
867           if (!cel->data()->userData().isEmpty()) {
868             someCelWithData = true;
869             break;
870           }
871         }
872 
873         if (someCelWithData) {
874           bool firstCel = true;
875 
876           os << ", \"cels\": [";
877           for (const Cel* cel : cels) {
878             if (!cel->data()->userData().isEmpty()) {
879               if (firstCel)
880                 firstCel = false;
881               else
882                 os << ", ";
883 
884               os << "{ \"frame\": " << cel->frame()
885                  << cel->data()->userData()
886                  << " }";
887             }
888           }
889           os << "]";
890         }
891 
892         os << " }";
893       }
894     }
895     os << "\n  ]";
896   }
897 
898   // meta.slices
899   if (m_listSlices) {
900     os << ",\n"
901        << "  \"slices\": [";
902 
903     bool firstSlice = true;
904     for (auto& item : m_documents) {
905       Doc* doc = item.doc;
906       Sprite* sprite = doc->sprite();
907 
908       // TODO add possibility to export some slices
909 
910       for (Slice* slice : sprite->slices()) {
911         if (firstSlice)
912           firstSlice = false;
913         else
914           os << ",";
915         os << "\n   { \"name\": \"" << escape_for_json(slice->name()) << "\""
916            << slice->userData();
917 
918         // Keys
919         if (!slice->empty()) {
920           bool firstKey = true;
921 
922           os << ", \"keys\": [";
923           for (const auto& key : *slice) {
924             if (firstKey)
925               firstKey = false;
926             else
927               os << ", ";
928 
929             const SliceKey* sliceKey = key.value();
930 
931             os << "{ \"frame\": " << key.frame() << ", "
932                << "\"bounds\": {"
933                << "\"x\": " << sliceKey->bounds().x << ", "
934                << "\"y\": " << sliceKey->bounds().y << ", "
935                << "\"w\": " << sliceKey->bounds().w << ", "
936                << "\"h\": " << sliceKey->bounds().h << " }";
937 
938             if (!sliceKey->center().isEmpty()) {
939               os << ", \"center\": {"
940                  << "\"x\": " << sliceKey->center().x << ", "
941                  << "\"y\": " << sliceKey->center().y << ", "
942                  << "\"w\": " << sliceKey->center().w << ", "
943                  << "\"h\": " << sliceKey->center().h << " }";
944             }
945 
946             if (sliceKey->hasPivot()) {
947               os << ", \"pivot\": {"
948                  << "\"x\": " << sliceKey->pivot().x << ", "
949                  << "\"y\": " << sliceKey->pivot().y << " }";
950             }
951 
952             os << " }";
953           }
954           os << "]";
955         }
956         os << " }";
957       }
958     }
959     os << "\n  ]";
960   }
961 
962   os << "\n }\n"
963      << "}\n";
964 }
965 
renderSample(const Sample & sample,doc::Image * dst,int x,int y) const966 void DocExporter::renderSample(const Sample& sample, doc::Image* dst, int x, int y) const
967 {
968   gfx::Clip clip(x, y, sample.trimmedBounds());
969 
970   RestoreVisibleLayers layersVisibility;
971   if (sample.selectedLayers())
972     layersVisibility.showSelectedLayers(sample.sprite(),
973                                         *sample.selectedLayers());
974 
975   render::Render render;
976   render.renderSprite(dst, sample.sprite(), sample.frame(), clip);
977 }
978 
979 } // namespace app
980