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