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 | |
60 | using namespace doc; |
61 | |
62 | namespace { |
63 | |
64 | std::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 | |
72 | std::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 | |
92 | namespace app { |
93 | |
94 | typedef std::shared_ptr<gfx::Rect> SharedRectPtr; |
95 | |
96 | DocExporter::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 | |
109 | DocExporter::Item::Item(Doc* doc, |
110 | const doc::ImageRef& image) |
111 | : doc(doc) |
112 | , image(image) |
113 | { |
114 | } |
115 | |
116 | DocExporter::Item::Item(Item&& other) = default; |
117 | DocExporter::Item::~Item() = default; |
118 | |
119 | int 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 | |
131 | doc::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 | |
149 | class DocExporter::Sample { |
150 | public: |
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 = 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 | |
310 | private: |
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 | |
329 | class DocExporter::Samples { |
330 | public: |
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 | |
351 | private: |
352 | List m_samples; |
353 | }; |
354 | |
355 | class DocExporter::LayoutSamples { |
356 | public: |
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 | |
365 | class DocExporter::SimpleLayoutSamples : public DocExporter::LayoutSamples { |
366 | public: |
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 | |
519 | private: |
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 | |
528 | class DocExporter::BestFitLayoutSamples : public DocExporter::LayoutSamples { |
529 | public: |
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 | |
592 | DocExporter::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 | |
600 | void 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 | |
628 | void DocExporter::setDocImageBuffer(const doc::ImageBufferPtr& docBuf) |
629 | { |
630 | m_docBuf = docBuf; |
631 | } |
632 | |
633 | Doc* 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 | |
724 | gfx::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 | |
733 | void 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 | |
744 | void 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 | |
752 | int 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 | |
850 | int 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 | |
880 | void 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 | |
1100 | void 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 | |
1128 | gfx::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 | |
1171 | Doc* 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 | |
1240 | void 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 | |
1282 | void 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 | |
1313 | void 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 | |