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 | |
42 | namespace app { |
43 | |
44 | namespace { |
45 | |
46 | std::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 | |
57 | bool 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 | |
94 | bool 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). |
108 | std::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 |
146 | void 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 | |
173 | CliProcessor::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 | |
183 | int 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 | |
634 | bool 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 | |
715 | void 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 | |