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/cli/cli_processor.h"
13
14#include "app/cli/app_options.h"
15#include "app/cli/cli_delegate.h"
16#include "app/commands/commands.h"
17#include "app/commands/params.h"
18#include "app/console.h"
19#include "app/doc.h"
20#include "app/doc_exporter.h"
21#include "app/doc_undo.h"
22#include "app/file/file.h"
23#include "app/filename_formatter.h"
24#include "app/restore_visible_layers.h"
25#include "app/ui_context.h"
26#include "base/convert_to.h"
27#include "base/fs.h"
28#include "base/split_string.h"
29#include "doc/layer.h"
30#include "doc/selected_frames.h"
31#include "doc/selected_layers.h"
32#include "doc/slice.h"
33#include "doc/tag.h"
34#include "doc/tags.h"
35#include "os/system.h"
36#include "render/dithering_algorithm.h"
37
38#include <algorithm>
39#include <queue>
40#include <vector>
41
42namespace app {
43
44namespace {
45
46std::string get_layer_path(const Layer* layer)
47{
48 std::string path;
49 for (; layer != layer->sprite()->root(); layer=layer->parent()) {
50 if (!path.empty())
51 path.insert(0, "/");
52 path.insert(0, layer->name());
53 }
54 return path;
55}
56
57bool match_path(const std::string& filter,
58 const std::string& layer_path,
59 const bool exclude)
60{
61 if (filter == layer_path)
62 return true;
63
64 std::vector<std::string> a, b;
65 base::split_string(filter, a, "/");
66 base::split_string(layer_path, b, "/");
67
68 for (std::size_t i=0; i<a.size() && i<b.size(); ++i) {
69 if (a[i] != b[i] && a[i] != "*")
70 return false;
71 }
72
73 const bool wildcard = (!a.empty() && a[a.size()-1] == "*");
74
75 // Exclude group itself when all children are excluded. This special
76 // case is only for exclusion because if we leave the group
77 // selected, the propagation of the selection will include all
78 // visible children too (see SelectedLayers::propagateSelection()).
79 if (exclude &&
80 a.size() > 1 &&
81 a.size() == b.size()+1 &&
82 wildcard) {
83 return true;
84 }
85
86 if (exclude || wildcard)
87 return (a.size() <= b.size());
88 else {
89 // Include filters need exact match when there is no wildcard
90 return (a.size() == b.size());
91 }
92}
93
94bool filter_layer(const std::string& layer_path,
95 const std::vector<std::string>& filters,
96 const bool result)
97{
98 for (const auto& filter : filters) {
99 if (match_path(filter, layer_path, !result))
100 return result;
101 }
102 return !result;
103}
104
105// If there is one layer with the given name "filter", we can convert
106// the filter to a full path to the layer (e.g. to match child layers
107// of a group).
108std::string convert_filter_to_layer_path_if_possible(
109 const Sprite* sprite,
110 const std::string& filter)
111{
112 std::string fullName;
113 std::queue<Layer*> layers;
114 layers.push(sprite->root());
115
116 while (!layers.empty()) {
117 const Layer* layer = layers.front();
118 layers.pop();
119
120 if (layer != sprite->root() &&
121 layer->name() == filter) {
122 if (fullName.empty()) {
123 fullName = get_layer_path(layer);
124 }
125 else {
126 // Two or more layers with the same name (use "filter" as a
127 // general filter, not a specific layer name)
128 return filter;
129 }
130 }
131 if (layer->isGroup()) {
132 for (auto child : static_cast<const LayerGroup*>(layer)->layers())
133 layers.push(child);
134 }
135 }
136
137 if (!fullName.empty())
138 return fullName;
139 else
140 return filter;
141}
142
143} // anonymous namespace
144
145// static
146void CliProcessor::FilterLayers(const Sprite* sprite,
147 std::vector<std::string> includes,
148 std::vector<std::string> excludes,
149 SelectedLayers& filteredLayers)
150{
151 // Convert filters to full paths for the sprite layers if there are
152 // just one layer with the given name.
153 for (auto& include : includes)
154 include = convert_filter_to_layer_path_if_possible(sprite, include);
155 for (auto& exclude : excludes)
156 exclude = convert_filter_to_layer_path_if_possible(sprite, exclude);
157
158 for (Layer* layer : sprite->allLayers()) {
159 auto layer_path = get_layer_path(layer);
160
161 if ((includes.empty() && !layer->isVisibleHierarchy()) ||
162 (!includes.empty() && !filter_layer(layer_path, includes, true)))
163 continue;
164
165 if (!excludes.empty() &&
166 !filter_layer(layer_path, excludes, false))
167 continue;
168
169 filteredLayers.insert(layer);
170 }
171}
172
173CliProcessor::CliProcessor(CliDelegate* delegate,
174 const AppOptions& options)
175 : m_delegate(delegate)
176 , m_options(options)
177 , m_exporter(nullptr)
178{
179 if (options.hasExporterParams())
180 m_exporter.reset(new DocExporter);
181}
182
183int CliProcessor::process(Context* ctx)
184{
185 // --help
186 if (m_options.showHelp()) {
187 m_delegate->showHelp(m_options);
188 }
189 // --version
190 else if (m_options.showVersion()) {
191 m_delegate->showVersion();
192 }
193 // Process other options and file names
194 else if (!m_options.values().empty()) {
195#ifdef ENABLE_SCRIPTING
196 Params scriptParams;
197#endif
198 Console console;
199 CliOpenFile cof;
200 SpriteSheetType sheetType = SpriteSheetType::None;
201 Doc* lastDoc = nullptr;
202 render::DitheringAlgorithm ditheringAlgorithm = render::DitheringAlgorithm::None;
203 std::string ditheringMatrix;
204
205 for (const auto& value : m_options.values()) {
206 const AppOptions::Option* opt = value.option();
207
208 // Special options/commands
209 if (opt) {
210 // --data <file.json>
211 if (opt == &m_options.data()) {
212 if (m_exporter)
213 m_exporter->setDataFilename(value.value());
214 }
215 // --format <format>
216 else if (opt == &m_options.format()) {
217 if (m_exporter) {
218 SpriteSheetDataFormat format = SpriteSheetDataFormat::Default;
219
220 if (value.value() == "json-hash")
221 format = SpriteSheetDataFormat::JsonHash;
222 else if (value.value() == "json-array")
223 format = SpriteSheetDataFormat::JsonArray;
224
225 m_exporter->setDataFormat(format);
226 }
227 }
228 // --sheet <file.png>
229 else if (opt == &m_options.sheet()) {
230 if (m_exporter)
231 m_exporter->setTextureFilename(value.value());
232 }
233 // --sheet-width <width>
234 else if (opt == &m_options.sheetWidth()) {
235 if (m_exporter)
236 m_exporter->setTextureWidth(strtol(value.value().c_str(), nullptr, 0));
237 }
238 // --sheet-height <height>
239 else if (opt == &m_options.sheetHeight()) {
240 if (m_exporter)
241 m_exporter->setTextureHeight(strtol(value.value().c_str(), nullptr, 0));
242 }
243 // --sheet-columns <columns>
244 else if (opt == &m_options.sheetColumns()) {
245 if (m_exporter)
246 m_exporter->setTextureColumns(strtol(value.value().c_str(), nullptr, 0));
247 }
248 // --sheet-rows <rows>
249 else if (opt == &m_options.sheetRows()) {
250 if (m_exporter)
251 m_exporter->setTextureRows(strtol(value.value().c_str(), nullptr, 0));
252 }
253 // --sheet-type <sheet-type>
254 else if (opt == &m_options.sheetType()) {
255 if (value.value() == "horizontal")
256 sheetType = SpriteSheetType::Horizontal;
257 else if (value.value() == "vertical")
258 sheetType = SpriteSheetType::Vertical;
259 else if (value.value() == "rows")
260 sheetType = SpriteSheetType::Rows;
261 else if (value.value() == "columns")
262 sheetType = SpriteSheetType::Columns;
263 else if (value.value() == "packed")
264 sheetType = SpriteSheetType::Packed;
265 }
266 // --sheet-pack
267 else if (opt == &m_options.sheetPack()) {
268 sheetType = SpriteSheetType::Packed;
269 }
270 // --split-layers
271 else if (opt == &m_options.splitLayers()) {
272 cof.splitLayers = true;
273 if (m_exporter)
274 m_exporter->setSplitLayers(true);
275 }
276 // --split-tags
277 else if (opt == &m_options.splitTags()) {
278 cof.splitTags = true;
279 if (m_exporter)
280 m_exporter->setSplitTags(true);
281 }
282 // --split-slice
283 else if (opt == &m_options.splitSlices()) {
284 cof.splitSlices = true;
285 }
286 // --split-grid
287 else if (opt == &m_options.splitGrid()) {
288 cof.splitGrid = true;
289 }
290 // --layer <layer-name>
291 else if (opt == &m_options.layer()) {
292 cof.includeLayers.push_back(value.value());
293 }
294 // --ignore-layer <layer-name>
295 else if (opt == &m_options.ignoreLayer()) {
296 cof.excludeLayers.push_back(value.value());
297 }
298 // --all-layers
299 else if (opt == &m_options.allLayers()) {
300 cof.allLayers = true;
301 }
302 // --tag <tag-name>
303 else if (opt == &m_options.tag()) {
304 cof.tag = value.value();
305 }
306 // --frame-range from,to
307 else if (opt == &m_options.frameRange()) {
308 std::vector<std::string> splitRange;
309 base::split_string(value.value(), splitRange, ",");
310 if (splitRange.size() < 2)
311 throw std::runtime_error("--frame-range needs two parameters separated by comma (,)\n"
312 "Usage: --frame-range from,to\n"
313 "E.g. --frame-range 0,99");
314
315 cof.fromFrame = base::convert_to<frame_t>(splitRange[0]);
316 cof.toFrame = base::convert_to<frame_t>(splitRange[1]);
317 }
318 // --ignore-empty
319 else if (opt == &m_options.ignoreEmpty()) {
320 cof.ignoreEmpty = true;
321 if (m_exporter)
322 m_exporter->setIgnoreEmptyCels(true);
323 }
324 // --merge-duplicates
325 else if (opt == &m_options.mergeDuplicates()) {
326 if (m_exporter)
327 m_exporter->setMergeDuplicates(true);
328 }
329 // --border-padding
330 else if (opt == &m_options.borderPadding()) {
331 if (m_exporter)
332 m_exporter->setBorderPadding(strtol(value.value().c_str(), NULL, 0));
333 }
334 // --shape-padding
335 else if (opt == &m_options.shapePadding()) {
336 if (m_exporter)
337 m_exporter->setShapePadding(strtol(value.value().c_str(), NULL, 0));
338 }
339 // --inner-padding
340 else if (opt == &m_options.innerPadding()) {
341 if (m_exporter)
342 m_exporter->setInnerPadding(strtol(value.value().c_str(), NULL, 0));
343 }
344 // --trim
345 else if (opt == &m_options.trim()) {
346 cof.trim = true;
347 if (m_exporter)
348 m_exporter->setTrimCels(true);
349 }
350 // --trim-sprite
351 else if (opt == &m_options.trimSprite()) {
352 cof.trim = true;
353 if (m_exporter)
354 m_exporter->setTrimSprite(true);
355 }
356 // --trim-by-grid
357 else if (opt == &m_options.trimByGrid()) {
358 cof.trim = cof.trimByGrid = true;
359 if (m_exporter) {
360 m_exporter->setTrimCels(true);
361 m_exporter->setTrimByGrid(true);
362 }
363 }
364 // --extrude
365 else if (opt == &m_options.extrude()) {
366 if (m_exporter)
367 m_exporter->setExtrude(true);
368 }
369 // --crop x,y,width,height
370 else if (opt == &m_options.crop()) {
371 std::vector<std::string> parts;
372 base::split_string(value.value(), parts, ",");
373 if (parts.size() < 4)
374 throw std::runtime_error("--crop needs four parameters separated by comma (,)\n"
375 "Usage: --crop x,y,width,height\n"
376 "E.g. --crop 0,0,32,32");
377
378 cof.crop.x = base::convert_to<int>(parts[0]);
379 cof.crop.y = base::convert_to<int>(parts[1]);
380 cof.crop.w = base::convert_to<int>(parts[2]);
381 cof.crop.h = base::convert_to<int>(parts[3]);
382 }
383 // --slice <slice>
384 else if (opt == &m_options.slice()) {
385 cof.slice = value.value();
386 }
387 // --filename-format
388 else if (opt == &m_options.filenameFormat()) {
389 cof.filenameFormat = value.value();
390 if (m_exporter)
391 m_exporter->setFilenameFormat(cof.filenameFormat);
392 }
393 // --save-as <filename>
394 else if (opt == &m_options.saveAs()) {
395 if (lastDoc) {
396 std::string fn = value.value();
397
398 // Automatic --split-layer, --split-tags, --split-slices
399 // in case the output filename already contains {layer},
400 // {tag}, or {slice} template elements.
401 bool hasLayerTemplate = (is_layer_in_filename_format(fn) ||
402 is_group_in_filename_format(fn));
403 bool hasTagTemplate = is_tag_in_filename_format(fn);
404 bool hasSliceTemplate = is_slice_in_filename_format(fn);
405
406 if (hasLayerTemplate || hasTagTemplate || hasSliceTemplate) {
407 cof.splitLayers = (cof.splitLayers || hasLayerTemplate);
408 cof.splitTags = (cof.splitTags || hasTagTemplate);
409 cof.splitSlices = (cof.splitSlices || hasSliceTemplate);
410 cof.filenameFormat =
411 get_default_filename_format(
412 fn,
413 true, // With path
414 (lastDoc->sprite()->totalFrames() > 1), // Has frames
415 false, // Has layer
416 false); // Has frame tag
417 }
418
419 cof.document = lastDoc;
420 cof.filename = fn;
421 saveFile(ctx, cof);
422 }
423 else
424 console.printf("A document is needed before --save-as argument\n");
425 }
426 // --palette <filename>
427 else if (opt == &m_options.palette()) {
428 if (lastDoc) {
429 ASSERT(cof.document == lastDoc);
430
431 std::string filename = value.value();
432 m_delegate->loadPalette(ctx, cof, filename);
433 }
434 else {
435 console.printf("You need to load a document to change its palette with --palette\n");
436 }
437 }
438 // --scale <factor>
439 else if (opt == &m_options.scale()) {
440 Params params;
441 params.set("scale", value.value().c_str());
442
443 // Scale all sprites
444 for (auto doc : ctx->documents()) {
445 ctx->setActiveDocument(doc);
446 ctx->executeCommand(Commands::instance()->byId(CommandId::SpriteSize()),
447 params);
448 }
449 }
450 // --dithering-algorithm <algorithm>
451 else if (opt == &m_options.ditheringAlgorithm()) {
452 if (value.value() == "none")
453 ditheringAlgorithm = render::DitheringAlgorithm::None;
454 else if (value.value() == "ordered")
455 ditheringAlgorithm = render::DitheringAlgorithm::Ordered;
456 else if (value.value() == "old")
457 ditheringAlgorithm = render::DitheringAlgorithm::Old;
458 else if (value.value() == "error-diffusion")
459 ditheringAlgorithm = render::DitheringAlgorithm::ErrorDiffusion;
460 else
461 throw std::runtime_error("--dithering-algorithm needs a valid algorithm name\n"
462 "Usage: --dithering-algorithm <algorithm>\n"
463 "Where <algorithm> can be none, ordered, old, or error-diffusion");
464 }
465 // --dithering-matrix <id>
466 else if (opt == &m_options.ditheringMatrix()) {
467 ditheringMatrix = value.value();
468 }
469 // --color-mode <mode>
470 else if (opt == &m_options.colorMode()) {
471 Command* command = Commands::instance()->byId(CommandId::ChangePixelFormat());
472 Params params;
473 if (value.value() == "rgb") {
474 params.set("format", "rgb");
475 }
476 else if (value.value() == "grayscale") {
477 params.set("format", "grayscale");
478 }
479 else if (value.value() == "indexed") {
480 params.set("format", "indexed");
481 switch (ditheringAlgorithm) {
482 case render::DitheringAlgorithm::None:
483 params.set("dithering", "none");
484 break;
485 case render::DitheringAlgorithm::Ordered:
486 params.set("dithering", "ordered");
487 break;
488 case render::DitheringAlgorithm::Old:
489 params.set("dithering", "old");
490 break;
491 case render::DitheringAlgorithm::ErrorDiffusion:
492 params.set("dithering", "error-diffusion");
493 break;
494 }
495
496 if (ditheringAlgorithm != render::DitheringAlgorithm::None &&
497 !ditheringMatrix.empty()) {
498 params.set("dithering-matrix", ditheringMatrix.c_str());
499 }
500 }
501 else {
502 throw std::runtime_error("--color-mode needs a valid color mode for conversion\n"
503 "Usage: --color-mode <mode>\n"
504 "Where <mode> can be rgb, grayscale, or indexed");
505 }
506
507 for (auto doc : ctx->documents()) {
508 ctx->setActiveDocument(doc);
509 ctx->executeCommand(command, params);
510 }
511 }
512 // --shrink-to <width,height>
513 else if (opt == &m_options.shrinkTo()) {
514 std::vector<std::string> dimensions;
515 base::split_string(value.value(), dimensions, ",");
516 if (dimensions.size() < 2)
517 throw std::runtime_error("--shrink-to needs two parameters separated by comma (,)\n"
518 "Usage: --shrink-to width,height\n"
519 "E.g. --shrink-to 128,64");
520
521 double maxWidth = base::convert_to<double>(dimensions[0]);
522 double maxHeight = base::convert_to<double>(dimensions[1]);
523 double scaleWidth, scaleHeight, scale;
524
525 // Shrink all sprites if needed
526 for (auto doc : ctx->documents()) {
527 ctx->setActiveDocument(doc);
528 scaleWidth = (doc->width() > maxWidth ? maxWidth / doc->width() : 1.0);
529 scaleHeight = (doc->height() > maxHeight ? maxHeight / doc->height() : 1.0);
530 if (scaleWidth < 1.0 || scaleHeight < 1.0) {
531 scale = std::min(scaleWidth, scaleHeight);
532 Params params;
533 params.set("scale", base::convert_to<std::string>(scale).c_str());
534 ctx->executeCommand(Commands::instance()->byId(CommandId::SpriteSize()),
535 params);
536 }
537 }
538 }
539#ifdef ENABLE_SCRIPTING
540 // --script <filename>
541 else if (opt == &m_options.script()) {
542 std::string filename = value.value();
543 int code;
544 try {
545 code = m_delegate->execScript(filename, scriptParams);
546 }
547 catch (const std::exception& ex) {
548 Console::showException(ex);
549 return -1;
550 }
551 if (code != 0)
552 return code;
553 }
554 // --script-param <name=value>
555 else if (opt == &m_options.scriptParam()) {
556 const std::string& v = value.value();
557 auto i = v.find('=');
558 if (i != std::string::npos)
559 scriptParams.set(v.substr(0, i).c_str(),
560 v.substr(i+1).c_str());
561 else
562 scriptParams.set(v.c_str(), "1");
563 }
564#endif
565 // --list-layers
566 else if (opt == &m_options.listLayers()) {
567 if (m_exporter)
568 m_exporter->setListLayers(true);
569 else
570 cof.listLayers = true;
571 }
572 // --list-tags
573 else if (opt == &m_options.listTags()) {
574 if (m_exporter)
575 m_exporter->setListTags(true);
576 else
577 cof.listTags = true;
578 }
579 // --list-slices
580 else if (opt == &m_options.listSlices()) {
581 if (m_exporter)
582 m_exporter->setListSlices(true);
583 else
584 cof.listSlices = true;
585 }
586 // --oneframe
587 else if (opt == &m_options.oneFrame()) {
588 cof.oneFrame = true;
589 }
590 // --export-tileset
591 else if (opt == &m_options.exportTileset()) {
592 cof.exportTileset = true;
593 }
594 }
595 // File names aren't associated to any option
596 else {
597 cof.document = nullptr;
598 cof.filename = base::normalize_path(value.value());
599
600 if (// Check that the filename wasn't used loading a sequence
601 // of images as one sprite
602 m_usedFiles.find(cof.filename) == m_usedFiles.end() &&
603 // Open sprite
604 openFile(ctx, cof)) {
605 lastDoc = cof.document;
606 }
607 }
608 }
609
610 if (m_exporter) {
611 // Rows sprite sheet as the default type
612 if (sheetType == SpriteSheetType::None)
613 sheetType = SpriteSheetType::Rows;
614 m_exporter->setSpriteSheetType(sheetType);
615
616 m_delegate->exportFiles(ctx, *m_exporter.get());
617 m_exporter.reset(nullptr);
618 }
619 }
620
621 // Running mode
622 if (m_options.startUI()) {
623 m_delegate->uiMode();
624 }
625 else if (m_options.startShell()) {
626 m_delegate->shellMode();
627 }
628 else {
629 m_delegate->batchMode();
630 }
631 return 0;
632}
633
634bool CliProcessor::openFile(Context* ctx, CliOpenFile& cof)
635{
636 m_delegate->beforeOpenFile(cof);
637
638 Doc* oldDoc = ctx->activeDocument();
639
640 m_batch.open(ctx,
641 cof.filename,
642 cof.oneFrame);
643
644 // Mark used file names as "already processed" so we don't try to
645 // open then again
646 for (const auto& usedFn : m_batch.usedFiles()) {
647 auto fn = base::normalize_path(usedFn);
648 m_usedFiles.insert(fn);
649
650 os::instance()->markCliFileAsProcessed(fn);
651 }
652
653 Doc* doc = ctx->activeDocument();
654 // If the active document is equal to the previous one, it
655 // means that we couldn't open this specific document.
656 if (doc == oldDoc)
657 doc = nullptr;
658
659 cof.document = doc;
660
661 if (doc) {
662 // Show all layers
663 if (cof.allLayers) {
664 for (doc::Layer* layer : doc->sprite()->allLayers())
665 layer->setVisible(true);
666 }
667
668 // Add document to exporter
669 if (m_exporter) {
670 Tag* tag = nullptr;
671 SelectedFrames selFrames;
672
673 if (cof.hasTag()) {
674 tag = doc->sprite()->tags().getByName(cof.tag);
675 }
676 if (cof.hasFrameRange()) {
677 // --frame-range with --frame-tag
678 if (tag) {
679 selFrames.insert(
680 tag->fromFrame()+std::clamp(cof.fromFrame, 0, tag->frames()-1),
681 tag->fromFrame()+std::clamp(cof.toFrame, 0, tag->frames()-1));
682 }
683 // --frame-range without --frame-tag
684 else {
685 selFrames.insert(cof.fromFrame, cof.toFrame);
686 }
687 }
688
689 SelectedLayers filteredLayers;
690 if (cof.hasLayersFilter())
691 filterLayers(doc->sprite(), cof, filteredLayers);
692
693 if (cof.exportTileset) {
694 m_exporter->addTilesetsSamples(
695 doc,
696 (cof.hasLayersFilter() ? &filteredLayers: nullptr));
697 }
698 else {
699 m_exporter->addDocumentSamples(
700 doc, tag,
701 cof.splitLayers,
702 cof.splitTags,
703 cof.splitGrid,
704 (cof.hasLayersFilter() ? &filteredLayers: nullptr),
705 (!selFrames.empty() ? &selFrames: nullptr));
706 }
707 }
708 }
709
710 m_delegate->afterOpenFile(cof);
711
712 return (doc ? true: false);
713}
714
715void CliProcessor::saveFile(Context* ctx, const CliOpenFile& cof)
716{
717 ctx->setActiveDocument(cof.document);
718
719 Command* trimCommand = Commands::instance()->byId(CommandId::AutocropSprite());
720 Command* undoCommand = Commands::instance()->byId(CommandId::Undo());
721 Doc* doc = cof.document;
722 bool clearUndo = false;
723
724 if (!cof.crop.isEmpty()) {
725 Params cropParams;
726 cropParams.set("x", base::convert_to<std::string>(cof.crop.x).c_str());
727 cropParams.set("y", base::convert_to<std::string>(cof.crop.y).c_str());
728 cropParams.set("width", base::convert_to<std::string>(cof.crop.w).c_str());
729 cropParams.set("height", base::convert_to<std::string>(cof.crop.h).c_str());
730 ctx->executeCommand(
731 Commands::instance()->byId(CommandId::CropSprite()),
732 cropParams);
733 }
734
735 std::string fn = cof.filename;
736 std::string filenameFormat = cof.filenameFormat;
737 if (filenameFormat.empty()) { // Default format
738 bool hasFrames = (cof.roi().frames() > 1);
739 filenameFormat = get_default_filename_format(
740 fn,
741 true, // With path
742 hasFrames, // Has frames
743 cof.splitLayers, // Has layer
744 cof.splitTags); // Has frame tag
745 }
746
747 SelectedLayers filteredLayers;
748 LayerList layers;
749 // --save-as with --split-layers or --split-tags
750 if (cof.splitLayers) {
751 for (doc::Layer* layer : doc->sprite()->allVisibleLayers())
752 layers.push_back(layer);
753 }
754 else {
755 // Filter layers
756 if (cof.hasLayersFilter())
757 filterLayers(doc->sprite(), cof, filteredLayers);
758
759 // All visible layers
760 layers.push_back(nullptr);
761 }
762
763 std::vector<doc::Tag*> tags;
764 if (cof.hasTag()) {
765 tags.push_back(
766 doc->sprite()->tags().getByName(cof.tag));
767 }
768 else {
769 doc::Tags& origTags = cof.document->sprite()->tags();
770 if (cof.splitTags && !origTags.empty()) {
771 for (doc::Tag* tag : origTags) {
772 // In case the tag is outside the given --frame-range
773 if (cof.hasFrameRange()) {
774 if (tag->toFrame() < cof.fromFrame ||
775 tag->fromFrame() > cof.toFrame)
776 continue;
777 }
778 tags.push_back(tag);
779 }
780 }
781 else
782 tags.push_back(nullptr);
783 }
784
785 std::vector<doc::Slice*> slices;
786 if (cof.hasSlice()) {
787 slices.push_back(
788 doc->sprite()->slices().getByName(cof.slice));
789 }
790 else {
791 doc::Slices& origSlices = cof.document->sprite()->slices();
792 if (cof.splitSlices && !origSlices.empty()) {
793 for (doc::Slice* slice : origSlices)
794 slices.push_back(slice);
795 }
796 else
797 slices.push_back(nullptr);
798 }
799
800 bool layerInFormat = is_layer_in_filename_format(fn);
801 bool groupInFormat = is_group_in_filename_format(fn);
802
803 for (doc::Slice* slice : slices) {
804 for (doc::Tag* tag : tags) {
805 // For each layer, hide other ones and save the sprite.
806 for (doc::Layer* layer : layers) {
807 RestoreVisibleLayers layersVisibility;
808
809 if (cof.splitLayers) {
810 ASSERT(layer);
811
812 // If the user doesn't want all layers and this one is hidden.
813 if (!layer->isVisible())
814 continue; // Just ignore this layer.
815
816 // Make this layer ("show") the only one visible.
817 layersVisibility.showLayer(layer);
818 }
819 else if (!filteredLayers.empty())
820 layersVisibility.showSelectedLayers(doc->sprite(), filteredLayers);
821
822 if (layer) {
823 if ((layerInFormat && layer->isGroup()) ||
824 (!layerInFormat && groupInFormat && !layer->isGroup())) {
825 continue;
826 }
827 }
828
829 // TODO --trim --save-as --split-layers doesn't make too much
830 // sense as we lost the trim rectangle information (e.g. we
831 // don't have sheet .json) Also, we should trim each frame
832 // individually (a process that can be done only in
833 // FileOp::operate()).
834 if (cof.trim) {
835 Params params;
836 if (cof.trimByGrid) {
837 params.set("byGrid", "true");
838 }
839 ctx->executeCommand(trimCommand, params);
840 }
841
842 CliOpenFile itemCof = cof;
843 FilenameInfo fnInfo;
844 fnInfo.filename(fn);
845 if (layer) {
846 fnInfo.layerName(layer->name());
847
848 if (layer->isGroup())
849 fnInfo.groupName(layer->name());
850 else if (layer->parent() != layer->sprite()->root())
851 fnInfo.groupName(layer->parent()->name());
852
853 itemCof.includeLayers.push_back(layer->name());
854 }
855 if (tag) {
856 fnInfo
857 .innerTagName(tag->name())
858 .outerTagName(tag->name());
859 itemCof.tag = tag->name();
860 }
861 if (slice) {
862 fnInfo.sliceName(slice->name());
863 itemCof.slice = slice->name();
864 }
865 itemCof.filename = filename_formatter(filenameFormat, fnInfo);
866 itemCof.filenameFormat = filename_formatter(filenameFormat, fnInfo, false);
867
868 // Call delegate
869 m_delegate->saveFile(ctx, itemCof);
870
871 if (cof.trim) {
872 ctx->executeCommand(undoCommand);
873 clearUndo = true;
874 }
875 }
876 }
877 }
878
879 // Undo crop
880 if (!cof.crop.isEmpty()) {
881 ctx->executeCommand(undoCommand);
882 clearUndo = true;
883 }
884
885 if (clearUndo) {
886 // Just in case allow non-linear history is enabled
887 // we clear redo information
888 doc->undoHistory()->clearRedo();
889 }
890}
891
892} // namespace app
893