1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/doc_exporter.h"
13
14#include "app/cmd/set_pixel_format.h"
15#include "app/console.h"
16#include "app/context.h"
17#include "app/doc.h"
18#include "app/file/file.h"
19#include "app/filename_formatter.h"
20#include "app/restore_visible_layers.h"
21#include "app/snap_to_grid.h"
22#include "app/util/autocrop.h"
23#include "base/convert_to.h"
24#include "base/fs.h"
25#include "base/fstream_path.h"
26#include "base/replace_string.h"
27#include "base/string.h"
28#include "doc/algorithm/shrink_bounds.h"
29#include "doc/cel.h"
30#include "doc/image.h"
31#include "doc/images_map.h"
32#include "doc/images_map.h"
33#include "doc/layer.h"
34#include "doc/palette.h"
35#include "doc/primitives.h"
36#include "doc/selected_frames.h"
37#include "doc/selected_layers.h"
38#include "doc/slice.h"
39#include "doc/sprite.h"
40#include "doc/tag.h"
41#include "gfx/packing_rects.h"
42#include "gfx/rect_io.h"
43#include "gfx/size.h"
44#include "render/dithering.h"
45#include "render/ordered_dither.h"
46#include "render/render.h"
47#include "ver/info.h"
48
49#include <algorithm>
50#include <cstdio>
51#include <fstream>
52#include <iomanip>
53#include <iostream>
54#include <memory>
55#include <set>
56#include <vector>
57
58#define DX_TRACE(...) // TRACEARGS
59
60using namespace doc;
61
62namespace {
63
64std::string escape_for_json(const std::string& path)
65{
66 std::string res = path;
67 base::replace_string(res, "\\", "\\\\");
68 base::replace_string(res, "\"", "\\\"");
69 return res;
70}
71
72std::ostream& operator<<(std::ostream& os, const doc::UserData& data)
73{
74 doc::color_t color = data.color();
75 if (doc::rgba_geta(color)) {
76 os << ", \"color\": \"#"
77 << std::hex << std::setfill('0')
78 << std::setw(2) << (int)doc::rgba_getr(color)
79 << std::setw(2) << (int)doc::rgba_getg(color)
80 << std::setw(2) << (int)doc::rgba_getb(color)
81 << std::setw(2) << (int)doc::rgba_geta(color)
82 << std::dec
83 << "\"";
84 }
85 if (!data.text().empty())
86 os << ", \"data\": \"" << escape_for_json(data.text()) << "\"";
87 return os;
88}
89
90} // anonymous namespace
91
92namespace app {
93
94typedef std::shared_ptr<gfx::Rect> SharedRectPtr;
95
96DocExporter::Item::Item(Doc* doc,
97 const doc::Tag* tag,
98 const doc::SelectedLayers* selLayers,
99 const doc::SelectedFrames* selFrames,
100 const bool splitGrid)
101 : doc(doc)
102 , tag(tag)
103 , selLayers(selLayers ? std::make_unique<doc::SelectedLayers>(*selLayers): nullptr)
104 , selFrames(selFrames ? std::make_unique<doc::SelectedFrames>(*selFrames): nullptr)
105 , splitGrid(splitGrid)
106{
107}
108
109DocExporter::Item::Item(Doc* doc,
110 const doc::ImageRef& image)
111 : doc(doc)
112 , image(image)
113{
114}
115
116DocExporter::Item::Item(Item&& other) = default;
117DocExporter::Item::~Item() = default;
118
119int DocExporter::Item::frames() const
120{
121 if (selFrames)
122 return selFrames->size();
123 else if (tag) {
124 int result = tag->toFrame() - tag->fromFrame() + 1;
125 return std::clamp(result, 1, doc->sprite()->totalFrames());
126 }
127 else
128 return doc->sprite()->totalFrames();
129}
130
131doc::SelectedFrames DocExporter::Item::getSelectedFrames() const
132{
133 if (selFrames)
134 return *selFrames;
135
136 doc::SelectedFrames frames;
137 if (tag) {
138 frames.insert(std::clamp(tag->fromFrame(), 0, doc->sprite()->lastFrame()),
139 std::clamp(tag->toFrame(), 0, doc->sprite()->lastFrame()));
140 }
141 else if (isOneImageOnly())
142 frames.insert(0);
143 else {
144 frames.insert(0, doc->sprite()->lastFrame());
145 }
146 return frames;
147}
148
149class DocExporter::Sample {
150public:
151 Sample(const gfx::Size& size,
152 Doc* document,
153 Sprite* sprite,
154 const ImageRef& image,
155 SelectedLayers* selLayers,
156 frame_t frame,
157 const Tag* tag,
158 const std::string& filename,
159 const int innerPadding,
160 const bool extrude) :
161 m_document(document),
162 m_sprite(sprite),
163 m_image(image),
164 m_selLayers(selLayers),
165 m_frame(frame),
166 m_tag(tag),
167 m_filename(filename),
168 m_innerPadding(innerPadding),
169 m_extrude(extrude),
170 m_isLinked(false),
171 m_isDuplicated(false),
172 m_originalSize(size),
173 m_trimmedBounds(size),
174 m_inTextureBounds(std::make_shared<gfx::Rect>(size)) {
175 }
176
177 Doc* document() const { return m_document; }
178 Sprite* sprite() const { return m_sprite; }
179 Layer* layer() const {
180 return (m_selLayers && m_selLayers->size() == 1 ? *m_selLayers->begin():
181 nullptr);
182 }
183 const Tag* tag() const { return m_tag; }
184 SelectedLayers* selectedLayers() const { return m_selLayers; }
185 frame_t frame() const { return m_frame; }
186 std::string filename() const { return m_filename; }
187 const gfx::Size& originalSize() const { return m_originalSize; }
188 const gfx::Rect& trimmedBounds() const { return m_trimmedBounds; }
189 const gfx::Rect& inTextureBounds() const { return *m_inTextureBounds; }
190 const SharedRectPtr& sharedBounds() const { return m_inTextureBounds; }
191
192 gfx::Size requiredSize() const {
193 // if extrude option is enabled, an extra pixel is needed for each side
194 // left+right borders and top+bottom borders
195 int extraExtrudePixels = m_extrude ? 2 : 0;
196 gfx::Size size = m_trimmedBounds.size();
197 size.w += 2*m_innerPadding + extraExtrudePixels;
198 size.h += 2*m_innerPadding + extraExtrudePixels;
199 return size;
200 }
201
202 bool trimmed() const {
203 return (m_trimmedBounds.x > 0 ||
204 m_trimmedBounds.y > 0 ||
205 m_trimmedBounds.w != m_originalSize.w ||
206 m_trimmedBounds.h != m_originalSize.h);
207 }
208
209 void setTrimmedBounds(const gfx::Rect& bounds) {
210 // TODO we cannot assign an empty rectangle (samples that are
211 // completely trimmed out should be included as a sample of size 1x1)
212 ASSERT(!bounds.isEmpty());
213 m_trimmedBounds = bounds;
214 }
215
216 void setInTextureBounds(const gfx::Rect& bounds) {
217 ASSERT(!bounds.isEmpty());
218 *m_inTextureBounds = bounds;
219 }
220
221 void setSharedBounds(const SharedRectPtr& bounds) {
222 m_inTextureBounds = bounds;
223 }
224
225 bool isLinked() const { return m_isLinked; }
226 bool isDuplicated() const { return m_isDuplicated; }
227 bool isEmpty() const {
228 // TODO trimmed bounds cannot be empty now (samples that are
229 // completely trimmed out are included as a sample of size 1x1)
230 ASSERT(!m_trimmedBounds.isEmpty());
231 return m_trimmedBounds.isEmpty();
232 }
233
234 void setLinked() { m_isLinked = true; }
235 void setDuplicated() { m_isDuplicated = true; }
236
237 ImageRef createRender(ImageBufferPtr& imageBuf) {
238 ASSERT(m_sprite);
239
240 // We use the m_image as it is, it doesn't require a special
241 // render.
242 if (m_image)
243 return m_image;
244
245 ImageRef render(
246 Image::create(m_sprite->pixelFormat(),
247 m_trimmedBounds.w,
248 m_trimmedBounds.h,
249 imageBuf));
250 render->setMaskColor(m_sprite->transparentColor());
251 clear_image(render.get(), m_sprite->transparentColor());
252 renderSample(render.get(), 0, 0, false);
253 return render;
254 }
255
256 void renderSample(doc::Image* dst, int x, int y, bool extrude) const {
257 RestoreVisibleLayers layersVisibility;
258 if (m_selLayers)
259 layersVisibility.showSelectedLayers(m_sprite,
260 *m_selLayers);
261
262 render::Render render;
263
264 // 1) We cannot use the Preferences because this is called from a non-UI thread
265 // 2) We should use the new blend mode always when we're saving files
266 //render.setNewBlend(Preferences::instance().experimental.newBlend());
267
268 if (extrude) {
269 const gfx::Rect& trim = m_trimmedBounds;
270
271 // Displaced position onto the destination texture
272 int dx[] = { 0, 1, trim.w+1 };
273 int dy[] = { 0, 1, trim.h+1 };
274
275 // Starting point of the area to be copied from the original image
276 // taking into account the size of the trimmed sprite
277 int srcx[] = { trim.x, trim.x, trim.x2()-1 };
278 int srcy[] = { trim.y, trim.y, trim.y2()-1 };
279
280 // Size of the area to be copied from original image, starting at
281 // the point (srcx[i], srxy[j])
282 int szx[] = { 1, trim.w, 1 };
283 int szy[] = { 1, trim.h, 1 };
284
285 // Render a 9-patch image extruding the sample one pixel on each
286 // side.
287 for (int j=0; j<3; ++j) {
288 for (int i=0; i<3; ++i) {
289 gfx::Clip clip(x+dx[i], y+dy[j], gfx::RectT<int>(srcx[i], srcy[j], szx[i], szy[j]));
290 if (m_image) {
291 dst->copy(m_image.get(), clip);
292 }
293 else {
294 render.renderSprite(dst, m_sprite, m_frame, clip);
295 }
296 }
297 }
298 }
299 else {
300 gfx::Clip clip(x, y, m_trimmedBounds);
301 if (m_image) {
302 dst->copy(m_image.get(), clip);
303 }
304 else {
305 render.renderSprite(dst, m_sprite, m_frame, clip);
306 }
307 }
308 }
309
310private:
311 Doc* m_document;
312 Sprite* m_sprite;
313 // In case that this Sample references just one image to export
314 // (e.g. like a Tileset tile image) this can be != nullptr.
315 ImageRef m_image;
316 SelectedLayers* m_selLayers;
317 frame_t m_frame;
318 const Tag* m_tag;
319 std::string m_filename;
320 int m_innerPadding;
321 bool m_extrude;
322 bool m_isLinked;
323 bool m_isDuplicated;
324 gfx::Size m_originalSize;
325 gfx::Rect m_trimmedBounds;
326 SharedRectPtr m_inTextureBounds;
327};
328
329class DocExporter::Samples {
330public:
331 typedef std::vector<Sample> List;
332 typedef List::iterator iterator;
333 typedef List::const_iterator const_iterator;
334
335 bool empty() const { return m_samples.empty(); }
336 int size() const { return int(m_samples.size()); }
337
338 void addSample(const Sample& sample) {
339 m_samples.push_back(sample);
340 }
341
342 const Sample& operator[](const size_t i) const {
343 return m_samples[i];
344 }
345
346 iterator begin() { return m_samples.begin(); }
347 iterator end() { return m_samples.end(); }
348 const_iterator begin() const { return m_samples.begin(); }
349 const_iterator end() const { return m_samples.end(); }
350
351private:
352 List m_samples;
353};
354
355class DocExporter::LayoutSamples {
356public:
357 virtual ~LayoutSamples() { }
358 virtual void layoutSamples(Samples& samples,
359 int borderPadding,
360 int shapePadding,
361 int& width, int& height,
362 base::task_token& token) = 0;
363};
364
365class DocExporter::SimpleLayoutSamples : public DocExporter::LayoutSamples {
366public:
367 SimpleLayoutSamples(SpriteSheetType type,
368 int maxCols, int maxRows,
369 bool splitLayers, bool splitTags,
370 bool mergeDups)
371 : m_type(type)
372 , m_maxCols(maxCols)
373 , m_maxRows(maxRows)
374 , m_splitLayers(splitLayers)
375 , m_splitTags(splitTags)
376 , m_mergeDups(mergeDups) {
377 }
378
379 void layoutSamples(Samples& samples,
380 int borderPadding,
381 int shapePadding,
382 int& width, int& height,
383 base::task_token& token) override {
384 DX_TRACE("DX: SimpleLayoutSamples type", (int)m_type, width, height);
385
386 const bool breakBands =
387 (m_type == SpriteSheetType::Columns ||
388 m_type == SpriteSheetType::Rows);
389
390 const Sprite* oldSprite = nullptr;
391 const Layer* oldLayer = nullptr;
392 const Tag* oldTag = nullptr;
393
394 doc::ImagesMap duplicates;
395 gfx::Point framePt(borderPadding, borderPadding);
396 gfx::Size rowSize(0, 0);
397
398 int i = 0;
399 int itemInBand = 0;
400 int itemsPerBand = -1;
401 if (breakBands) {
402 if (m_type == SpriteSheetType::Columns && m_maxRows > 0)
403 itemsPerBand = m_maxRows;
404 if (m_type == SpriteSheetType::Rows && m_maxCols > 0)
405 itemsPerBand = m_maxCols;
406 }
407
408 for (auto& sample : samples) {
409 if (token.canceled())
410 return;
411 token.set_progress(0.2f + 0.2f * i / samples.size());
412
413 if (sample.isEmpty()) {
414 sample.setInTextureBounds(gfx::Rect(0, 0, 0, 0));
415 ++i;
416 continue;
417 }
418
419 if (m_mergeDups || sample.isLinked()) {
420 doc::ImageBufferPtr sampleBuf = std::make_shared<doc::ImageBuffer>();
421 doc::ImageRef sampleRender(sample.createRender(sampleBuf));
422 auto it = duplicates.find(sampleRender);
423 if (it != duplicates.end()) {
424 const uint32_t j = it->second;
425
426 sample.setDuplicated();
427 sample.setSharedBounds(samples[j].sharedBounds());
428 ++i;
429 continue;
430 }
431 else {
432 duplicates[sampleRender] = i;
433 }
434 }
435
436 const Sprite* sprite = sample.sprite();
437 const Layer* layer = sample.layer();
438 const Tag* tag = sample.tag();
439 gfx::Size size = sample.requiredSize();
440
441 if (breakBands && oldSprite) {
442 const bool nextBand =
443 ((oldSprite != sprite) ||
444 (m_splitLayers && oldLayer != layer) ||
445 (m_splitTags && oldTag != tag) ||
446 (itemInBand == itemsPerBand));
447
448 if (m_type == SpriteSheetType::Columns) {
449 // If the user didn't specify a height for the texture, we
450 // put each sprite/layer in a different column.
451 if (height == 0) {
452 // New sprite or layer, go to next column.
453 if (nextBand) {
454 framePt.x += rowSize.w + shapePadding;
455 framePt.y = borderPadding;
456 rowSize = size;
457 itemInBand = 0;
458 }
459 }
460 // When a texture height is specified, we can put different
461 // sprites/layers in each column until we reach the texture
462 // bottom-border.
463 else if (framePt.y+size.h > height-borderPadding) {
464 framePt.x += rowSize.w + shapePadding;
465 framePt.y = borderPadding;
466 rowSize = size;
467 }
468 }
469 else if (m_type == SpriteSheetType::Rows) {
470 // If the user didn't specify a width for the texture, we put
471 // each sprite/layer in a different row.
472 if (width == 0) {
473 // New sprite or layer, go to next row.
474 if (nextBand) {
475 framePt.x = borderPadding;
476 framePt.y += rowSize.h + shapePadding;
477 rowSize = size;
478 itemInBand = 0;
479 }
480 }
481 // When a texture width is specified, we can put different
482 // sprites/layers in each row until we reach the texture
483 // right-border.
484 else if (framePt.x+size.w > width-borderPadding) {
485 framePt.x = borderPadding;
486 framePt.y += rowSize.h + shapePadding;
487 rowSize = size;
488 }
489 }
490 else {
491 ASSERT(false);
492 }
493 }
494
495 sample.setInTextureBounds(gfx::Rect(framePt, size));
496
497 // Next frame position.
498 if (m_type == SpriteSheetType::Vertical ||
499 m_type == SpriteSheetType::Columns) {
500 framePt.y += size.h + shapePadding;
501 }
502 else if (m_type == SpriteSheetType::Horizontal ||
503 m_type == SpriteSheetType::Rows) {
504 framePt.x += size.w + shapePadding;
505 }
506
507 rowSize = rowSize.createUnion(size);
508
509 oldSprite = sprite;
510 oldLayer = layer;
511 oldTag = tag;
512 ++itemInBand;
513 ++i;
514 }
515
516 DX_TRACE("DX: -> SimpleLayoutSamples", width, height);
517 }
518
519private:
520 SpriteSheetType m_type;
521 int m_maxCols;
522 int m_maxRows;
523 bool m_splitLayers;
524 bool m_splitTags;
525 bool m_mergeDups;
526};
527
528class DocExporter::BestFitLayoutSamples : public DocExporter::LayoutSamples {
529public:
530 void layoutSamples(Samples& samples,
531 int borderPadding,
532 int shapePadding,
533 int& width, int& height,
534 base::task_token& token) override {
535 gfx::PackingRects pr(borderPadding, shapePadding);
536 doc::ImagesMap duplicates;
537
538 uint32_t i = 0;
539 for (auto& sample : samples) {
540 if (token.canceled())
541 return;
542 token.set_progress_range(0.2f, 0.3f);
543 token.set_progress(float(i) / samples.size());
544
545 if (sample.isEmpty()) {
546 ++i;
547 continue;
548 }
549
550 // We have to use one ImageBuffer for each image because we're
551 // going to store all images in the "duplicates" map.
552 doc::ImageBufferPtr sampleBuf = std::make_shared<doc::ImageBuffer>();
553 doc::ImageRef sampleRender(sample.createRender(sampleBuf));
554 auto it = duplicates.find(sampleRender);
555 if (it != duplicates.end()) {
556 const uint32_t j = it->second;
557
558 sample.setDuplicated();
559 sample.setSharedBounds(samples[j].sharedBounds());
560 }
561 else {
562 duplicates[sampleRender] = i;
563 pr.add(sample.requiredSize());
564 }
565 ++i;
566 }
567
568 token.set_progress_range(0.3f, 0.4f);
569 if (width == 0 || height == 0) {
570 gfx::Size sz = pr.bestFit(token, width, height);
571 width = sz.w;
572 height = sz.h;
573 }
574 else {
575 pr.pack(gfx::Size(width, height), token);
576 }
577 token.set_progress_range(0.0f, 1.0f);
578
579 auto it = pr.begin();
580 for (auto& sample : samples) {
581 if (sample.isLinked() ||
582 sample.isDuplicated() ||
583 sample.isEmpty())
584 continue;
585
586 ASSERT(it != pr.end());
587 sample.setInTextureBounds(*(it++));
588 }
589 }
590};
591
592DocExporter::DocExporter()
593 : m_docBuf(std::make_shared<doc::ImageBuffer>())
594 , m_sampleBuf(std::make_shared<doc::ImageBuffer>())
595{
596 m_cache.spriteId = doc::NullId;
597 reset();
598}
599
600void DocExporter::reset()
601{
602 m_sheetType = SpriteSheetType::None;
603 m_dataFormat = SpriteSheetDataFormat::Default;
604 m_dataFilename.clear();
605 m_textureFilename.clear();
606 m_filenameFormat.clear();
607 m_textureWidth = 0;
608 m_textureHeight = 0;
609 m_textureColumns = 0;
610 m_textureRows = 0;
611 m_borderPadding = 0;
612 m_shapePadding = 0;
613 m_innerPadding = 0;
614 m_ignoreEmptyCels = false;
615 m_mergeDuplicates = false;
616 m_trimSprite = false;
617 m_trimCels = false;
618 m_trimByGrid = false;
619 m_extrude = false;
620 m_splitLayers = false;
621 m_splitTags = false;
622 m_listTags = false;
623 m_listLayers = false;
624 m_listSlices = false;
625 m_documents.clear();
626}
627
628void DocExporter::setDocImageBuffer(const doc::ImageBufferPtr& docBuf)
629{
630 m_docBuf = docBuf;
631}
632
633Doc* DocExporter::exportSheet(Context* ctx, base::task_token& token)
634{
635 // We output the metadata to std::cout if the user didn't specify a file.
636 std::ofstream fos;
637 std::streambuf* osbuf = nullptr;
638 if (m_dataFilename.empty()) {
639 // Redirect to stdout if we are running in batch mode
640 if (!ctx->isUIAvailable())
641 osbuf = std::cout.rdbuf();
642 }
643 else {
644 // Make missing directories for the json file
645 {
646 std::string dir = base::get_file_path(m_dataFilename);
647 try {
648 if (!base::is_directory(dir))
649 base::make_all_directories(dir);
650 }
651 catch (const std::exception& ex) {
652 Console console;
653 console.printf("Error creating directory \"%s\"\n%s",
654 dir.c_str(), ex.what());
655 }
656 }
657
658 fos.open(FSTREAM_PATH(m_dataFilename), std::ios::out);
659 osbuf = fos.rdbuf();
660 }
661 std::ostream os(osbuf);
662
663 // Steps for sheet construction:
664 // 1) Capture the samples (each sprite+frame pair)
665 Samples samples;
666 captureSamples(samples, token);
667 if (samples.empty()) {
668 if (!ctx->isUIAvailable()) {
669 Console console;
670 console.printf("No documents to export");
671 }
672 return nullptr;
673 }
674 if (token.canceled())
675 return nullptr;
676 token.set_progress(0.2f);
677
678 // 2) Layout those samples in a texture field.
679 layoutSamples(samples, token);
680 if (token.canceled())
681 return nullptr;
682 token.set_progress(0.4f);
683
684 // 3) Create and render the texture.
685 std::unique_ptr<Doc> textureDocument(
686 createEmptyTexture(samples, token));
687 if (token.canceled())
688 return nullptr;
689 token.set_progress(0.6f);
690
691 Sprite* texture = textureDocument->sprite();
692 Image* textureImage = texture->root()->firstLayer()
693 ->cel(frame_t(0))->image();
694
695 renderTexture(ctx, samples, textureImage, token);
696 if (token.canceled())
697 return nullptr;
698 token.set_progress(0.8f);
699
700 // Trim texture
701 if (m_trimSprite || m_trimCels)
702 trimTexture(samples, texture);
703 token.set_progress(0.9f);
704
705 // Save the metadata.
706 if (osbuf)
707 createDataFile(samples, os, texture);
708 token.set_progress(0.95f);
709
710 // Save the image files.
711 if (!m_textureFilename.empty()) {
712 DX_TRACE("DX: exportSheet", m_textureFilename);
713 textureDocument->setFilename(m_textureFilename.c_str());
714 int ret = save_document(ctx, textureDocument.get());
715 if (ret == 0)
716 textureDocument->markAsSaved();
717 }
718
719 token.set_progress(1.0f);
720
721 return textureDocument.release();
722}
723
724gfx::Size DocExporter::calculateSheetSize()
725{
726 base::task_token token;
727 Samples samples;
728 captureSamples(samples, token);
729 layoutSamples(samples, token);
730 return calculateSheetSize(samples, token);
731}
732
733void DocExporter::addDocument(
734 Doc* doc,
735 const doc::Tag* tag,
736 const doc::SelectedLayers* selLayers,
737 const doc::SelectedFrames* selFrames,
738 const bool splitGrid)
739{
740 DX_TRACE("DX: addDocument doc=", doc, "tag=", tag);
741 m_documents.push_back(Item(doc, tag, selLayers, selFrames, splitGrid));
742}
743
744void DocExporter::addImage(
745 Doc* doc,
746 const doc::ImageRef& image)
747{
748 DX_TRACE("DX: addImage doc=", doc, "image=", image.get());
749 m_documents.push_back(Item(doc, image));
750}
751
752int DocExporter::addDocumentSamples(
753 Doc* doc,
754 const doc::Tag* thisTag,
755 const bool splitLayers,
756 const bool splitTags,
757 const bool splitGrid,
758 const doc::SelectedLayers* selLayers,
759 const doc::SelectedFrames* selFrames)
760{
761 DX_TRACE("DX: addDocumentSamples");
762
763 std::vector<const Tag*> tags;
764
765 if (thisTag)
766 tags.push_back(thisTag);
767 else if (splitTags) {
768 if (selFrames) {
769 const Tag* oldTag = nullptr;
770 for (frame_t frame : *selFrames) {
771 const Tag* tag = doc->sprite()->tags().innerTag(frame);
772 if (oldTag != tag) {
773 oldTag = tag;
774 // Do not include untagged frames
775 if (tag)
776 tags.push_back(tag);
777 }
778 }
779 }
780 else {
781 for (const Tag* tag : doc->sprite()->tags())
782 tags.push_back(tag);
783 }
784 if (tags.empty())
785 tags.push_back(nullptr);
786 }
787 else {
788 tags.push_back(nullptr);
789 }
790
791 doc::SelectedFrames selFramesTmp;
792 int items = 0;
793 for (const Tag* tag : tags) {
794 const doc::SelectedFrames* thisSelFrames = nullptr;
795
796 if (selFrames) {
797 if (tag) {
798 selFramesTmp.clear();
799 for (frame_t frame=tag->fromFrame(); frame<=tag->toFrame(); ++frame) {
800 if (selFrames->contains(frame))
801 selFramesTmp.insert(frame);
802 }
803 thisSelFrames = &selFramesTmp;
804 }
805 else {
806 selFramesTmp = *selFrames;
807 thisSelFrames = &selFramesTmp;
808 }
809 }
810 else if (tag) {
811 ASSERT(tag);
812 selFramesTmp.clear();
813 selFramesTmp.insert(tag->fromFrame(),
814 tag->toFrame());
815 thisSelFrames = &selFramesTmp;
816 }
817
818 if (splitLayers) {
819 if (selLayers) {
820 for (auto layer : selLayers->toAllLayersList()) {
821 if (layer->isGroup()) // Ignore groups
822 continue;
823
824 SelectedLayers oneLayer;
825 oneLayer.insert(layer);
826 addDocument(doc, tag, &oneLayer, thisSelFrames, splitGrid);
827 ++items;
828 }
829 }
830 else {
831 for (auto layer : doc->sprite()->allVisibleLayers()) {
832 if (layer->isGroup()) // Ignore groups
833 continue;
834
835 SelectedLayers oneLayer;
836 oneLayer.insert(layer);
837 addDocument(doc, tag, &oneLayer, thisSelFrames, splitGrid);
838 ++items;
839 }
840 }
841 }
842 else {
843 addDocument(doc, tag, selLayers, thisSelFrames, splitGrid);
844 ++items;
845 }
846 }
847 return std::max(1, items);
848}
849
850int DocExporter::addTilesetsSamples(
851 Doc* doc,
852 const doc::SelectedLayers* selLayers)
853{
854 LayerList layers;
855 if (selLayers)
856 layers = selLayers->toAllLayersList();
857 else
858 layers = doc->sprite()->allVisibleLayers();
859
860 std::set<doc::ObjectId> alreadyExported;
861 int items = 0;
862 for (auto& layer : layers) {
863 if (layer->isTilemap()) {
864 Tileset* ts = dynamic_cast<LayerTilemap*>(layer)->tileset();
865
866 if (alreadyExported.find(ts->id()) == alreadyExported.end()) {
867 for (const ImageRef& image : *ts) {
868 addImage(doc, image);
869 ++items;
870 }
871 alreadyExported.insert(ts->id());
872 }
873 }
874 }
875
876 DX_TRACE("DX: addTilesetsSamples items=", items);
877 return items;
878}
879
880void DocExporter::captureSamples(Samples& samples,
881 base::task_token& token)
882{
883 DX_TRACE("DX: Capture samples");
884
885 for (auto& item : m_documents) {
886 if (token.canceled())
887 return;
888
889 Doc* doc = item.doc;
890 Sprite* sprite = doc->sprite();
891 Layer* layer = (item.selLayers && item.selLayers->size() == 1 ?
892 *item.selLayers->begin(): nullptr);
893 const Tag* tag = item.tag;
894 int frames = item.frames();
895
896 DX_TRACE("DX: - Item:", doc->filename(),
897 "Frames:", frames,
898 "Layer:", layer ? layer->name(): "-",
899 "Tag:", tag ? tag->name(): "-");
900
901 std::string format = m_filenameFormat;
902 if (format.empty()) {
903 format = get_default_filename_format_for_sheet(
904 doc->filename(),
905 (frames > 1), // Has frames
906 (layer != nullptr), // Has layer
907 (tag != nullptr)); // Has tag
908 }
909
910 gfx::Rect spriteBounds;
911
912 // This item is only one image (e.g. a tileset tile)
913 if (item.isOneImageOnly()) {
914 ASSERT(item.image);
915 spriteBounds = item.image->bounds();
916 }
917 // This item comes from the sprite canvas
918 else {
919 spriteBounds = sprite->bounds();
920 if (m_trimSprite) {
921 if (m_cache.spriteId == sprite->id() &&
922 m_cache.spriteVer == sprite->version() &&
923 m_cache.trimmedByGrid == m_trimByGrid) {
924 spriteBounds = m_cache.trimmedBounds;
925 }
926 else {
927 spriteBounds = get_trimmed_bounds(sprite, m_trimByGrid);
928 if (spriteBounds.isEmpty())
929 spriteBounds = gfx::Rect(0, 0, 1, 1);
930
931 // Cache trimmed bounds so we don't have to recalculate them
932 // in the next iteration/preview.
933 m_cache.spriteId = sprite->id();
934 m_cache.spriteVer = sprite->version();
935 m_cache.trimmedByGrid = m_trimByGrid;
936 m_cache.trimmedBounds = spriteBounds;
937 }
938 }
939 }
940
941 frame_t outputFrame = 0;
942 for (frame_t frame : item.getSelectedFrames()) {
943 if (token.canceled())
944 return;
945
946 const Tag* innerTag = (tag ? tag: sprite->tags().innerTag(frame));
947 const Tag* outerTag = sprite->tags().outerTag(frame);
948 FilenameInfo fnInfo;
949 fnInfo
950 .filename(doc->filename())
951 .layerName(layer ? layer->name(): "")
952 .groupName(layer && layer->parent() != sprite->root() ? layer->parent()->name(): "")
953 .innerTagName(innerTag ? innerTag->name(): "")
954 .outerTagName(outerTag ? outerTag->name(): "")
955 .frame(outputFrame)
956 .tagFrame(innerTag ? frame - innerTag->fromFrame():
957 outputFrame)
958 .duration(sprite->frameDuration(frame));
959 ++outputFrame;
960
961 std::string filename = filename_formatter(format, fnInfo);
962
963 Sample sample(
964 (item.image ? item.image->size():
965 item.splitGrid ? sprite->gridBounds().size():
966 sprite->size()),
967 doc, sprite, item.image, item.selLayers.get(),
968 frame, innerTag, filename,
969 m_innerPadding, m_extrude);
970 Cel* cel = nullptr;
971 Cel* link = nullptr;
972 bool done = false;
973
974 if (layer && layer->isImage()) {
975 cel = layer->cel(frame);
976 if (cel)
977 link = cel->link();
978 }
979
980 // Re-use linked samples
981 bool alreadyTrimmed = false;
982 if (link && m_mergeDuplicates &&
983 !item.isOneImageOnly()) {
984 for (const Sample& other : samples) {
985 if (token.canceled())
986 return;
987
988 if (other.sprite() == sprite &&
989 other.layer() == layer &&
990 other.frame() == link->frame()) {
991 ASSERT(!other.isLinked());
992
993 sample.setLinked();
994 sample.setTrimmedBounds(other.trimmedBounds());
995 sample.setSharedBounds(other.sharedBounds());
996 alreadyTrimmed = true;
997 done = true;
998 break;
999 }
1000 }
1001 // "done" variable can be false here, e.g. when we export a
1002 // frame tag and the first linked cel is outside the tag range.
1003 ASSERT(done || (!done && tag));
1004 }
1005
1006 if (!done && (m_ignoreEmptyCels || m_trimCels) &&
1007 !item.isOneImageOnly()) {
1008 // Ignore empty cels
1009 if (layer && layer->isImage() && !cel && m_ignoreEmptyCels)
1010 continue;
1011
1012 ImageRef sampleRender(sample.createRender(m_sampleBuf));
1013
1014 gfx::Rect frameBounds;
1015 doc::color_t refColor = 0;
1016
1017 if (m_trimCels) {
1018 if ((layer &&
1019 layer->isBackground()) ||
1020 (!layer &&
1021 sprite->backgroundLayer() &&
1022 sprite->backgroundLayer()->isVisible())) {
1023 refColor = get_pixel(sampleRender.get(), 0, 0);
1024 }
1025 else {
1026 refColor = sprite->transparentColor();
1027 }
1028 }
1029 else if (m_ignoreEmptyCels)
1030 refColor = sprite->transparentColor();
1031
1032 if (!algorithm::shrink_bounds(sampleRender.get(),
1033 refColor,
1034 nullptr, // layer
1035 spriteBounds, // startBounds
1036 frameBounds)) { // output bounds
1037 // If shrink_bounds() returns false, it's because the whole
1038 // image is transparent (equal to the mask color).
1039
1040 // Should we ignore this empty frame? (i.e. don't include
1041 // the frame in the sprite sheet)
1042 if (m_ignoreEmptyCels)
1043 continue;
1044
1045 // Create an entry with Size(1, 1) for this completely
1046 // trimmed frame anyway so we conserve the frame information
1047 // (position and duration of the frame in the JSON data, and
1048 // the relative position of the frame in frame tags).
1049 sample.setTrimmedBounds(frameBounds = gfx::Rect(0, 0, 1, 1));
1050 }
1051
1052 if (m_trimCels) {
1053 // TODO merge this code with the code in DocApi::trimSprite()
1054 if (m_trimByGrid) {
1055 const gfx::Rect& gridBounds = doc->sprite()->gridBounds();
1056 gfx::Point posTopLeft =
1057 snap_to_grid(gridBounds,
1058 frameBounds.origin(),
1059 PreferSnapTo::FloorGrid);
1060 gfx::Point posBottomRight =
1061 snap_to_grid(gridBounds,
1062 frameBounds.point2(),
1063 PreferSnapTo::CeilGrid);
1064 frameBounds = gfx::Rect(posTopLeft, posBottomRight);
1065 }
1066 sample.setTrimmedBounds(frameBounds);
1067 alreadyTrimmed = true;
1068 }
1069 }
1070 if (!alreadyTrimmed && m_trimSprite)
1071 sample.setTrimmedBounds(spriteBounds);
1072
1073 if (item.splitGrid) {
1074 const gfx::Rect& gridBounds = sprite->gridBounds();
1075 gfx::Point initPos(0, 0), pos;
1076 initPos = pos = snap_to_grid(gridBounds, initPos, PreferSnapTo::BoxOrigin);
1077
1078 for (; pos.y+gridBounds.h <= spriteBounds.h; pos.y+=gridBounds.h) {
1079 for (pos.x=initPos.x; pos.x+gridBounds.w <= spriteBounds.w; pos.x+=gridBounds.w) {
1080 const gfx::Rect cellBounds(pos, gridBounds.size());
1081 sample.setTrimmedBounds(cellBounds);
1082 sample.setSharedBounds(std::make_shared<gfx::Rect>(sample.inTextureBounds()));
1083 samples.addSample(sample);
1084 }
1085 }
1086 }
1087 else {
1088 samples.addSample(sample);
1089 }
1090
1091 DX_TRACE("DX: - Sample:",
1092 sample.document()->filename(),
1093 "Layer:", sample.layer() ? sample.layer()->name(): "-",
1094 "TrimmedBounds:", sample.trimmedBounds(),
1095 "InTextureBounds:", sample.inTextureBounds());
1096 }
1097 }
1098}
1099
1100void DocExporter::layoutSamples(Samples& samples,
1101 base::task_token& token)
1102{
1103 int width = m_textureWidth;
1104 int height = m_textureHeight;
1105
1106 switch (m_sheetType) {
1107 case SpriteSheetType::Packed: {
1108 BestFitLayoutSamples layout;
1109 layout.layoutSamples(
1110 samples, m_borderPadding, m_shapePadding,
1111 width, height, token);
1112 break;
1113 }
1114 default: {
1115 SimpleLayoutSamples layout(
1116 m_sheetType,
1117 m_textureColumns, m_textureRows,
1118 m_splitLayers, m_splitTags,
1119 m_mergeDuplicates);
1120 layout.layoutSamples(
1121 samples, m_borderPadding, m_shapePadding,
1122 width, height, token);
1123 break;
1124 }
1125 }
1126}
1127
1128gfx::Size DocExporter::calculateSheetSize(const Samples& samples,
1129 base::task_token& token) const
1130{
1131 DX_TRACE("DX: calculateSheetSize predefined texture size",
1132 m_textureWidth, m_textureHeight);
1133
1134 gfx::Rect fullTextureBounds(0, 0, m_textureWidth, m_textureHeight);
1135
1136 for (const auto& sample : samples) {
1137 if (token.canceled())
1138 return gfx::Size(0, 0);
1139
1140 if (sample.isLinked() ||
1141 sample.isDuplicated() ||
1142 sample.isEmpty())
1143 continue;
1144
1145 gfx::Rect sampleBounds = sample.inTextureBounds();
1146
1147 // If the user specified a fixed sprite sheet size, we add the
1148 // border padding in the sample size to do an union between
1149 // fullTextureBounds and sample's inTextureBounds (generally, it
1150 // shouldn't make fullTextureBounds bigger).
1151 if (m_textureWidth > 0) sampleBounds.w += m_borderPadding;
1152 if (m_textureHeight > 0) sampleBounds.h += m_borderPadding;
1153
1154 fullTextureBounds |= sampleBounds;
1155 }
1156
1157 // If the user didn't specified the sprite sheet size, the border is
1158 // added right here (the left/top border padding should be added by
1159 // the DocExporter::LayoutSamples() impl).
1160 if (m_textureWidth == 0) fullTextureBounds.w += m_borderPadding;
1161 if (m_textureHeight == 0) fullTextureBounds.h += m_borderPadding;
1162
1163 DX_TRACE("DX: calculateSheetSize -> ",
1164 fullTextureBounds.x+fullTextureBounds.w,
1165 fullTextureBounds.y+fullTextureBounds.h);
1166
1167 return gfx::Size(fullTextureBounds.x+fullTextureBounds.w,
1168 fullTextureBounds.y+fullTextureBounds.h);
1169}
1170
1171Doc* DocExporter::createEmptyTexture(const Samples& samples,
1172 base::task_token& token) const
1173{
1174 ColorMode colorMode = ColorMode::INDEXED;
1175 Palette* palette = nullptr;
1176 int maxColors = 256;
1177 gfx::ColorSpaceRef colorSpace;
1178 color_t transparentColor = 0;
1179
1180 for (const auto& sample : samples) {
1181 if (token.canceled())
1182 return nullptr;
1183
1184 if (sample.isLinked() ||
1185 sample.isDuplicated() ||
1186 sample.isEmpty())
1187 continue;
1188
1189 // TODO throw a warning if samples contain different color spaces
1190 if (!colorSpace) {
1191 if (sample.sprite())
1192 colorSpace = sample.sprite()->colorSpace();
1193 }
1194
1195 // We try to render an indexed image. But if we find a sprite with
1196 // two or more palettes, or two of the sprites have different
1197 // palettes, we've to use RGB format.
1198 if (colorMode == ColorMode::INDEXED) {
1199 if (sample.sprite()->colorMode() != ColorMode::INDEXED) {
1200 colorMode = ColorMode::RGB;
1201 }
1202 else if (sample.sprite()->getPalettes().size() > 1) {
1203 colorMode = ColorMode::RGB;
1204 }
1205 else if (palette &&
1206 palette->countDiff(sample.sprite()->palette(frame_t(0)),
1207 nullptr, nullptr) > 0) {
1208 colorMode = ColorMode::RGB;
1209 }
1210 else if (!palette) {
1211 palette = sample.sprite()->palette(frame_t(0));
1212 transparentColor = sample.sprite()->transparentColor();
1213 }
1214 }
1215 }
1216
1217 gfx::Size textureSize = calculateSheetSize(samples, token);
1218 if (token.canceled())
1219 return nullptr;
1220
1221 std::unique_ptr<Sprite> sprite(
1222 Sprite::MakeStdSprite(
1223 ImageSpec(colorMode,
1224 std::max(textureSize.w, m_textureWidth),
1225 std::max(textureSize.h, m_textureHeight),
1226 transparentColor,
1227 (colorSpace ? colorSpace: gfx::ColorSpace::MakeNone())),
1228 maxColors,
1229 m_docBuf));
1230
1231 if (palette)
1232 sprite->setPalette(palette, false);
1233
1234 std::unique_ptr<Doc> document(new Doc(sprite.get()));
1235 sprite.release();
1236
1237 return document.release();
1238}
1239
1240void DocExporter::renderTexture(Context* ctx,
1241 const Samples& samples,
1242 Image* textureImage,
1243 base::task_token& token) const
1244{
1245 textureImage->clear(textureImage->maskColor());
1246
1247 int i = 0;
1248 for (const auto& sample : samples) {
1249 if (token.canceled())
1250 return;
1251 token.set_progress(0.6f + 0.2f * i / int(samples.size()));
1252
1253 if (sample.isLinked() ||
1254 sample.isDuplicated() ||
1255 sample.isEmpty()) {
1256 ++i;
1257 continue;
1258 }
1259
1260 // Make the sprite compatible with the texture so the render()
1261 // works correctly.
1262 if (sample.sprite()->pixelFormat() != textureImage->pixelFormat()) {
1263 cmd::SetPixelFormat(
1264 sample.sprite(),
1265 textureImage->pixelFormat(),
1266 render::Dithering(),
1267 Sprite::DefaultRgbMapAlgorithm(), // TODO add rgbmap algorithm preference
1268 nullptr, // toGray is not needed because the texture is Indexed or RGB
1269 nullptr) // TODO add a delegate to show progress
1270 .execute(ctx);
1271 }
1272
1273 sample.renderSample(
1274 textureImage,
1275 sample.inTextureBounds().x+m_innerPadding,
1276 sample.inTextureBounds().y+m_innerPadding,
1277 m_extrude);
1278 ++i;
1279 }
1280}
1281
1282void DocExporter::trimTexture(const Samples& samples,
1283 doc::Sprite* texture) const
1284{
1285 if (m_textureWidth > 0 && m_textureHeight > 0)
1286 return;
1287
1288 gfx::Size size = texture->size();
1289 gfx::Rect bounds(0, 0, 1, 1);
1290
1291 for (const auto& sample : samples) {
1292 if (sample.isLinked() ||
1293 sample.isDuplicated() ||
1294 sample.isEmpty())
1295 continue;
1296
1297 bounds |= sample.inTextureBounds();
1298 }
1299
1300 if (m_textureWidth == 0) {
1301 ASSERT(size.w >= bounds.w);
1302 size.w = bounds.w;
1303 }
1304 if (m_textureHeight == 0) {
1305 ASSERT(size.h >= bounds.h);
1306 size.h = bounds.h;
1307 }
1308
1309 texture->setSize(m_textureWidth > 0 ? m_textureWidth: size.w,
1310 m_textureHeight > 0 ? m_textureHeight: size.h);
1311}
1312
1313void DocExporter::createDataFile(const Samples& samples,
1314 std::ostream& os,
1315 doc::Sprite* texture)
1316{
1317 std::string frames_begin;
1318 std::string frames_end;
1319 bool filename_as_key = false;
1320 bool filename_as_attr = false;
1321 int nonExtrudedPosition = 0;
1322 int nonExtrudedSize = 0;
1323
1324 // if the the image was extruded then the exported meta-information (JSON)
1325 // should inform where start the real image (+1 displaced) and its
1326 // size (-2 pixels: one per each dimension compared the extruded image)
1327 if (m_extrude) {
1328 nonExtrudedPosition += 1;
1329 nonExtrudedSize -= 2;
1330 }
1331
1332 // TODO we should use some string templates system here
1333 switch (m_dataFormat) {
1334 case SpriteSheetDataFormat::JsonHash:
1335 frames_begin = "{";
1336 frames_end = "}";
1337 filename_as_key = true;
1338 filename_as_attr = false;
1339 break;
1340 case SpriteSheetDataFormat::JsonArray:
1341 frames_begin = "[";
1342 frames_end = "]";
1343 filename_as_key = false;
1344 filename_as_attr = true;
1345 break;
1346 }
1347
1348 os << "{ \"frames\": " << frames_begin << "\n";
1349 for (Samples::const_iterator
1350 it = samples.begin(),
1351 end = samples.end(); it != end; ) {
1352 const Sample& sample = *it;
1353 gfx::Size srcSize = sample.originalSize();
1354 gfx::Rect spriteSourceBounds = sample.trimmedBounds();
1355 gfx::Rect frameBounds = sample.inTextureBounds();
1356
1357 if (filename_as_key)
1358 os << " \"" << escape_for_json(sample.filename()) << "\": {\n";
1359 else if (filename_as_attr)
1360 os << " {\n"
1361 << " \"filename\": \"" << escape_for_json(sample.filename()) << "\",\n";
1362
1363 os << " \"frame\": { "
1364 << "\"x\": " << frameBounds.x + nonExtrudedPosition << ", "
1365 << "\"y\": " << frameBounds.y + nonExtrudedPosition << ", "
1366 << "\"w\": " << frameBounds.w + nonExtrudedSize << ", "
1367 << "\"h\": " << frameBounds.h + nonExtrudedSize << " },\n"
1368 << " \"rotated\": false,\n"
1369 << " \"trimmed\": " << (sample.trimmed() ? "true": "false") << ",\n"
1370 << " \"spriteSourceSize\": { "
1371 << "\"x\": " << spriteSourceBounds.x << ", "
1372 << "\"y\": " << spriteSourceBounds.y << ", "
1373 << "\"w\": " << spriteSourceBounds.w << ", "
1374 << "\"h\": " << spriteSourceBounds.h << " },\n"
1375 << " \"sourceSize\": { "
1376 << "\"w\": " << srcSize.w << ", "
1377 << "\"h\": " << srcSize.h << " },\n"
1378 << " \"duration\": " << sample.sprite()->frameDuration(sample.frame()) << "\n"
1379 << " }";
1380
1381 if (++it != samples.end())
1382 os << ",\n";
1383 else
1384 os << "\n";
1385 }
1386 os << " " << frames_end;
1387
1388 // "meta" property
1389 os << ",\n"
1390 << " \"meta\": {\n"
1391 << " \"app\": \"" << get_app_url() << "\",\n"
1392 << " \"version\": \"" << get_app_version() << "\",\n";
1393
1394 if (!m_textureFilename.empty())
1395 os << " \"image\": \""
1396 << escape_for_json(base::get_file_name(m_textureFilename)).c_str()
1397 << "\",\n";
1398
1399 os << " \"format\": \"" << (texture->pixelFormat() == IMAGE_RGB ? "RGBA8888": "I8") << "\",\n"
1400 << " \"size\": { "
1401 << "\"w\": " << texture->width() << ", "
1402 << "\"h\": " << texture->height() << " },\n"
1403 << " \"scale\": \"1\"";
1404
1405 // meta.frameTags
1406 if (m_listTags) {
1407 os << ",\n"
1408 << " \"frameTags\": ["; // TODO rename this someday in the future
1409
1410 std::set<doc::ObjectId> includedSprites;
1411
1412 bool firstTag = true;
1413 for (auto& item : m_documents) {
1414 if (item.isOneImageOnly())
1415 continue;
1416
1417 Doc* doc = item.doc;
1418 Sprite* sprite = doc->sprite();
1419
1420 // Avoid including tags two or more times in the list (e.g. when
1421 // -split-layers is specified, several calls of addDocument()
1422 // are used for each layer, so we have to avoid iterating the
1423 // same sprite several times)
1424 if (includedSprites.find(sprite->id()) != includedSprites.end())
1425 continue;
1426 includedSprites.insert(sprite->id());
1427
1428 for (Tag* tag : sprite->tags()) {
1429 if (firstTag)
1430 firstTag = false;
1431 else
1432 os << ",";
1433
1434 os << "\n { \"name\": \"" << escape_for_json(tag->name()) << "\","
1435 << " \"from\": " << (tag->fromFrame()) << ","
1436 << " \"to\": " << (tag->toFrame()) << ","
1437 " \"direction\": \"" << escape_for_json(convert_anidir_to_string(tag->aniDir())) << "\"";
1438 os << tag->userData() << " }";
1439 }
1440 }
1441 os << "\n ]";
1442 }
1443
1444 // meta.layers
1445 if (m_listLayers) {
1446 LayerList metaLayers;
1447 for (auto& item : m_documents) {
1448 if (item.isOneImageOnly())
1449 continue;
1450
1451 Doc* doc = item.doc;
1452 Sprite* sprite = doc->sprite();
1453 Layer* root = sprite->root();
1454
1455 LayerList layers;
1456 if (item.selLayers) {
1457 // Select all layers (not only browseable ones)
1458 layers = item.selLayers->toAllLayersList();
1459 }
1460 else {
1461 // Select all visible layers by default
1462 layers = sprite->allVisibleLayers();
1463 }
1464
1465 for (Layer* layer : layers) {
1466 // If this layer is inside a group, check that the group will
1467 // be included in the meta data too.
1468 Layer* group = layer->parent();
1469 int pos = int(metaLayers.size());
1470 while (group && group != root) {
1471 if (std::find(metaLayers.begin(), metaLayers.end(), group) == metaLayers.end()) {
1472 metaLayers.insert(metaLayers.begin()+pos, group);
1473 }
1474 group = group->parent();
1475 }
1476 // Insert the layer
1477 if (std::find(metaLayers.begin(), metaLayers.end(), layer) == metaLayers.end()) {
1478 metaLayers.push_back(layer);
1479 }
1480 }
1481 }
1482
1483 bool firstLayer = true;
1484 os << ",\n"
1485 << " \"layers\": [";
1486 for (Layer* layer : metaLayers) {
1487 if (firstLayer)
1488 firstLayer = false;
1489 else
1490 os << ",";
1491 os << "\n { \"name\": \"" << escape_for_json(layer->name()) << "\"";
1492
1493 if (layer->parent() != layer->sprite()->root())
1494 os << ", \"group\": \"" << escape_for_json(layer->parent()->name()) << "\"";
1495
1496 if (LayerImage* layerImg = dynamic_cast<LayerImage*>(layer)) {
1497 os << ", \"opacity\": " << layerImg->opacity()
1498 << ", \"blendMode\": \"" << blend_mode_to_string(layerImg->blendMode()) << "\"";
1499 }
1500 os << layer->userData();
1501
1502 // Cels
1503 CelList cels;
1504 layer->getCels(cels);
1505 bool someCelWithData = false;
1506 for (const Cel* cel : cels) {
1507 if (!cel->data()->userData().isEmpty()) {
1508 someCelWithData = true;
1509 break;
1510 }
1511 }
1512
1513 if (someCelWithData) {
1514 bool firstCel = true;
1515
1516 os << ", \"cels\": [";
1517 for (const Cel* cel : cels) {
1518 if (!cel->data()->userData().isEmpty()) {
1519 if (firstCel)
1520 firstCel = false;
1521 else
1522 os << ", ";
1523
1524 os << "{ \"frame\": " << cel->frame()
1525 << cel->data()->userData()
1526 << " }";
1527 }
1528 }
1529 os << "]";
1530 }
1531
1532 os << " }";
1533 }
1534 os << "\n ]";
1535 }
1536
1537 // meta.slices
1538 if (m_listSlices) {
1539 os << ",\n"
1540 << " \"slices\": [";
1541
1542 std::set<doc::ObjectId> includedSprites;
1543
1544 bool firstSlice = true;
1545 for (auto& item : m_documents) {
1546 if (item.isOneImageOnly())
1547 continue;
1548
1549 Doc* doc = item.doc;
1550 Sprite* sprite = doc->sprite();
1551
1552 // Avoid including slices two or more times in the list
1553 // (e.g. when -split-layers is specified, several calls of
1554 // addDocument() are used for each layer, so we have to avoid
1555 // iterating the same sprite several times)
1556 if (includedSprites.find(sprite->id()) != includedSprites.end())
1557 continue;
1558 includedSprites.insert(sprite->id());
1559
1560 // TODO add possibility to export some slices
1561
1562 for (Slice* slice : sprite->slices()) {
1563 if (firstSlice)
1564 firstSlice = false;
1565 else
1566 os << ",";
1567 os << "\n { \"name\": \"" << escape_for_json(slice->name()) << "\""
1568 << slice->userData();
1569
1570 // Keys
1571 if (!slice->empty()) {
1572 bool firstKey = true;
1573
1574 os << ", \"keys\": [";
1575 for (const auto& key : *slice) {
1576 if (firstKey)
1577 firstKey = false;
1578 else
1579 os << ", ";
1580
1581 const SliceKey* sliceKey = key.value();
1582
1583 os << "{ \"frame\": " << key.frame() << ", "
1584 << "\"bounds\": {"
1585 << "\"x\": " << sliceKey->bounds().x << ", "
1586 << "\"y\": " << sliceKey->bounds().y << ", "
1587 << "\"w\": " << sliceKey->bounds().w << ", "
1588 << "\"h\": " << sliceKey->bounds().h << " }";
1589
1590 if (!sliceKey->center().isEmpty()) {
1591 os << ", \"center\": {"
1592 << "\"x\": " << sliceKey->center().x << ", "
1593 << "\"y\": " << sliceKey->center().y << ", "
1594 << "\"w\": " << sliceKey->center().w << ", "
1595 << "\"h\": " << sliceKey->center().h << " }";
1596 }
1597
1598 if (sliceKey->hasPivot()) {
1599 os << ", \"pivot\": {"
1600 << "\"x\": " << sliceKey->pivot().x << ", "
1601 << "\"y\": " << sliceKey->pivot().y << " }";
1602 }
1603
1604 os << " }";
1605 }
1606 os << "]";
1607 }
1608 os << " }";
1609 }
1610 }
1611 os << "\n ]";
1612 }
1613
1614 os << "\n }\n"
1615 << "}\n";
1616}
1617
1618} // namespace app
1619