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/file/file.h"
13
14#include "app/cmd/convert_color_profile.h"
15#include "app/color_spaces.h"
16#include "app/console.h"
17#include "app/context.h"
18#include "app/doc.h"
19#include "app/drm.h"
20#include "app/file/file_data.h"
21#include "app/file/file_format.h"
22#include "app/file/file_formats_manager.h"
23#include "app/file/format_options.h"
24#include "app/file/split_filename.h"
25#include "app/filename_formatter.h"
26#include "app/i18n/strings.h"
27#include "app/modules/gui.h"
28#include "app/modules/palettes.h"
29#include "app/pref/preferences.h"
30#include "app/tx.h"
31#include "app/ui/optional_alert.h"
32#include "app/ui/status_bar.h"
33#include "base/fs.h"
34#include "base/string.h"
35#include "dio/detect_format.h"
36#include "doc/algorithm/resize_image.h"
37#include "doc/doc.h"
38#include "fmt/format.h"
39#include "render/quantization.h"
40#include "render/render.h"
41#include "ui/alert.h"
42#include "ui/listitem.h"
43#include "ui/system.h"
44#include "ver/info.h"
45
46#include "ask_for_color_profile.xml.h"
47#include "open_sequence.xml.h"
48
49#include <cstring>
50#include <cstdarg>
51
52namespace app {
53
54using namespace base;
55
56class FileOp::FileAbstractImageImpl : public FileAbstractImage {
57public:
58 FileAbstractImageImpl(FileOp* fop)
59 : m_doc(fop->document())
60 , m_sprite(m_doc->sprite())
61 , m_spec(m_sprite->spec())
62 , m_newBlend(fop->newBlend()) {
63 ASSERT(m_doc && m_sprite);
64 }
65
66 void setSpecSize(const gfx::Size& size) {
67 m_spec.setWidth(size.w * m_scale.x);
68 m_spec.setHeight(size.h * m_scale.y);
69 }
70
71 void setUnscaledImage(const doc::frame_t frame,
72 const doc::ImageRef& image) {
73 if (m_spec.width() == image->width() &&
74 m_spec.height() == image->height()) {
75 m_tmpScaledImage = image;
76 }
77 else {
78 if (!m_tmpScaledImage)
79 m_tmpScaledImage.reset(doc::Image::create(m_spec));
80
81 doc::algorithm::resize_image(
82 image.get(),
83 m_tmpScaledImage.get(),
84 doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR,
85 palette(frame),
86 m_sprite->rgbMap(frame),
87 image->maskColor());
88 }
89 }
90
91 // FileAbstractImage impl
92 const doc::ImageSpec& spec() const override {
93 return m_spec;
94 }
95
96 os::ColorSpaceRef osColorSpace() const override {
97 return m_doc->osColorSpace();
98 }
99
100 bool needAlpha() const override {
101 return m_sprite->needAlpha();
102 }
103
104 bool isOpaque() const override {
105 return m_sprite->isOpaque();
106 }
107
108 int frames() const override {
109 return m_sprite->totalFrames();
110 }
111
112 int frameDuration(doc::frame_t frame) const override {
113 return m_sprite->frameDuration(frame);
114 }
115
116 const doc::Palette* palette(doc::frame_t frame) const override {
117 ASSERT(m_sprite);
118 return m_sprite->palette(frame);
119 }
120
121 doc::PalettesList palettes() const override {
122 ASSERT(m_sprite);
123 return m_sprite->getPalettes();
124 }
125
126 const doc::ImageRef getScaledImage() const override {
127 return m_tmpScaledImage;
128 }
129
130 const uint8_t* getScanline(int y) const override {
131 return m_tmpScaledImage->getPixelAddress(0, y);
132 }
133
134 void renderFrame(const doc::frame_t frame, doc::Image* dst) const override {
135 const bool needResize =
136 (dst->width() != m_sprite->width() ||
137 dst->height() != m_sprite->height());
138
139 if (needResize && !m_tmpUnscaledRender) {
140 auto spec = m_sprite->spec();
141 spec.setColorMode(dst->colorMode());
142 m_tmpUnscaledRender.reset(doc::Image::create(spec));
143 }
144
145 render::Render render;
146 render.setNewBlend(m_newBlend);
147 render.setBgOptions(render::BgOptions::MakeNone());
148 render.renderSprite(
149 (needResize ? m_tmpUnscaledRender.get(): dst),
150 m_sprite, frame);
151
152 if (needResize) {
153 doc::algorithm::resize_image(
154 m_tmpUnscaledRender.get(),
155 dst,
156 doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR,
157 palette(frame),
158 m_sprite->rgbMap(frame),
159 m_tmpUnscaledRender->maskColor());
160 }
161 }
162
163 void setScale(const gfx::PointF& scale) {
164 m_scale = scale;
165 m_spec.setWidth(m_spec.width() * m_scale.x);
166 m_spec.setHeight(m_spec.height() * m_scale.y);
167 }
168
169private:
170 const Doc* m_doc;
171 const doc::Sprite* m_sprite;
172 doc::ImageSpec m_spec;
173 bool m_newBlend;
174 doc::ImageRef m_tmpScaledImage = nullptr;
175 mutable doc::ImageRef m_tmpUnscaledRender = nullptr;
176 gfx::PointF m_scale = gfx::PointF(1.0, 1.0);
177};
178
179base::paths get_readable_extensions()
180{
181 base::paths paths;
182 for (const FileFormat* format : *FileFormatsManager::instance()) {
183 if (format->support(FILE_SUPPORT_LOAD))
184 format->getExtensions(paths);
185 }
186 return paths;
187}
188
189base::paths get_writable_extensions(const int requiredFormatFlag)
190{
191 base::paths paths;
192 for (const FileFormat* format : *FileFormatsManager::instance()) {
193 if (format->support(FILE_SUPPORT_SAVE) &&
194 (requiredFormatFlag == 0 ||
195 format->support(requiredFormatFlag)))
196 format->getExtensions(paths);
197 }
198 return paths;
199}
200
201Doc* load_document(Context* context, const std::string& filename)
202{
203 /* TODO add a option to configure what to do with the sequence */
204 std::unique_ptr<FileOp> fop(
205 FileOp::createLoadDocumentOperation(
206 context, filename,
207 FILE_LOAD_CREATE_PALETTE |
208 FILE_LOAD_SEQUENCE_NONE));
209 if (!fop)
210 return nullptr;
211
212 // Operate in this same thread
213 fop->operate();
214 fop->done();
215 fop->postLoad();
216
217 if (fop->hasError()) {
218 Console console(context);
219 console.printf(fop->error().c_str());
220 }
221
222 Doc* document = fop->releaseDocument();
223 if (document && context)
224 document->setContext(context);
225
226 return document;
227}
228
229int save_document(Context* context, Doc* document)
230{
231 std::unique_ptr<FileOp> fop(
232 FileOp::createSaveDocumentOperation(
233 context,
234 FileOpROI(document, gfx::Rect(), "", "", SelectedFrames(), false),
235 document->filename(), "",
236 false));
237 if (!fop)
238 return -1;
239
240 // Operate in this same thread
241 fop->operate();
242 fop->done();
243
244 if (fop->hasError()) {
245 Console console(context);
246 console.printf(fop->error().c_str());
247 }
248
249 return (!fop->hasError() ? 0: -1);
250}
251
252bool is_static_image_format(const std::string& filename)
253{
254 // Get the format through the extension of the filename
255 FileFormat* format =
256 FileFormatsManager::instance()
257 ->getFileFormat(dio::detect_format_by_file_extension(filename));
258
259 return (format && format->support(FILE_SUPPORT_SEQUENCES));
260}
261
262FileOpROI::FileOpROI()
263 : m_document(nullptr)
264 , m_slice(nullptr)
265 , m_tag(nullptr)
266{
267}
268
269FileOpROI::FileOpROI(const Doc* doc,
270 const gfx::Rect& bounds,
271 const std::string& sliceName,
272 const std::string& tagName,
273 const doc::SelectedFrames& selFrames,
274 const bool adjustByTag)
275 : m_document(doc)
276 , m_bounds(bounds)
277 , m_slice(nullptr)
278 , m_tag(nullptr)
279 , m_selFrames(selFrames)
280{
281 if (doc) {
282 if (!sliceName.empty())
283 m_slice = doc->sprite()->slices().getByName(sliceName);
284
285 // Don't allow exporting frame tags with empty names
286 if (!tagName.empty())
287 m_tag = doc->sprite()->tags().getByName(tagName);
288
289 if (m_tag) {
290 if (m_selFrames.empty())
291 m_selFrames.insert(m_tag->fromFrame(), m_tag->toFrame());
292 else if (adjustByTag)
293 m_selFrames.displace(m_tag->fromFrame());
294
295 m_selFrames =
296 m_selFrames.filter(std::max(0, m_tag->fromFrame()),
297 std::min(m_tag->toFrame(), doc->sprite()->lastFrame()));
298 }
299 // All frames if selected frames is empty
300 else if (m_selFrames.empty())
301 m_selFrames.insert(0, doc->sprite()->lastFrame());
302 }
303}
304
305// static
306FileOp* FileOp::createLoadDocumentOperation(Context* context,
307 const std::string& filename,
308 const int flags,
309 const FileOpConfig* config)
310{
311 std::unique_ptr<FileOp> fop(
312 new FileOp(FileOpLoad, context, config));
313 if (!fop)
314 return nullptr;
315
316 LOG("FILE: Loading file \"%s\"\n", filename.c_str());
317
318 // Does file exist?
319 if (!base::is_file(filename)) {
320 fop->setError("File not found: \"%s\"\n", filename.c_str());
321 goto done;
322 }
323
324 // Get the format through the extension of the filename
325 fop->m_format = FileFormatsManager::instance()->getFileFormat(
326 dio::detect_format(filename));
327 if (!fop->m_format ||
328 !fop->m_format->support(FILE_SUPPORT_LOAD)) {
329 fop->setError("%s can't load \"%s\" file (\"%s\")\n", get_app_name(),
330 filename.c_str(), base::get_file_extension(filename).c_str());
331 goto done;
332 }
333
334 // Use the "sequence" interface
335 if (fop->m_format->support(FILE_SUPPORT_SEQUENCES)) {
336 fop->prepareForSequence();
337 fop->m_seq.flags = flags;
338
339 // At the moment we want load just one file (the one specified in filename)
340 fop->m_seq.filename_list.push_back(filename);
341
342 // If the user wants to load the whole sequence
343 if (!(flags & FILE_LOAD_SEQUENCE_NONE)) {
344 std::string left, right;
345 int c, width, start_from;
346 char buf[512];
347
348 // First of all, we must generate the list of files to load in the
349 // sequence...
350
351 // Check is this could be a sequence
352 start_from = split_filename(filename, left, right, width);
353 if (start_from >= 0) {
354 // Try to get more file names
355 for (c=start_from+1; ; c++) {
356 // Get the next file name
357 sprintf(buf, "%s%0*d%s", left.c_str(), width, c, right.c_str());
358
359 // If the file doesn't exist, we doesn't need more files to load
360 if (!base::is_file(buf))
361 break;
362
363 /* add this file name to the list */
364 fop->m_seq.filename_list.push_back(buf);
365 }
366 }
367
368#ifdef ENABLE_UI
369 // TODO add a better dialog to edit file-names
370 if ((flags & FILE_LOAD_SEQUENCE_ASK) &&
371 context &&
372 context->isUIAvailable() &&
373 fop->m_seq.filename_list.size() > 1) {
374 app::gen::OpenSequence window;
375 window.repeat()->setVisible(flags & FILE_LOAD_SEQUENCE_ASK_CHECKBOX ? true: false);
376
377 for (const auto& fn : fop->m_seq.filename_list) {
378 auto item = new ui::ListItem(base::get_file_name(fn));
379 item->setSelected(true);
380 window.files()->addChild(item);
381 }
382
383 window.files()->Change.connect(
384 [&window]{
385 window.agree()->setEnabled(
386 window.files()->getSelectedChild() != nullptr);
387 });
388
389 window.duration()->setTextf("%d", fop->m_seq.duration);
390 window.duration()->Change.connect(
391 [&]() {
392 fop->m_seq.duration = window.duration()->textInt();
393 // If the animation duration is changed we'll prefer to
394 // agree on loading the sequence if the user press Enter.
395 //
396 // TODO maybe the "Agree" button should be the default
397 // focus magnet in this dialog
398 window.agree()->setFocusMagnet(true);
399 });
400
401 window.openWindowInForeground();
402
403 // Don't show this alert again.
404 if (window.dontShow()->isSelected()) {
405 Preferences::instance().openFile.openSequence(
406 window.closer() == window.agree() ?
407 gen::SequenceDecision::YES:
408 gen::SequenceDecision::NO);
409 }
410
411 // If the user selected the "do the same for other files"
412 // checkbox, we've to save what the user want to do for the
413 // following files.
414 if (window.repeat()->isSelected() ||
415 window.dontShow()->isSelected()) {
416 if (window.closer() == window.agree())
417 fop->m_seq.flags = FILE_LOAD_SEQUENCE_YES;
418 else
419 fop->m_seq.flags = FILE_LOAD_SEQUENCE_NONE;
420 }
421
422 if (window.closer() == window.agree()) {
423 // If the user replies "Agree", we load the selected files.
424 base::paths list;
425
426 auto it = window.files()->children().begin();
427 auto end = window.files()->children().end();
428 for (const auto& fn : fop->m_seq.filename_list) {
429 ASSERT(it != end);
430 if (it == end)
431 break;
432 if ((*it)->isSelected())
433 list.push_back(fn);
434 ++it;
435 }
436
437 ASSERT(!list.empty());
438 fop->m_seq.filename_list = list;
439 }
440 else {
441 // If the user replies "Skip", we need just one file name
442 // (the first one).
443 if (fop->m_seq.filename_list.size() > 1) {
444 fop->m_seq.filename_list.erase(fop->m_seq.filename_list.begin()+1,
445 fop->m_seq.filename_list.end());
446 }
447 }
448 }
449#endif // ENABLE_UI
450 }
451 }
452 else {
453 fop->m_filename = filename;
454 }
455
456 // Load just one frame
457 if (flags & FILE_LOAD_ONE_FRAME)
458 fop->m_oneframe = true;
459
460 if (flags & FILE_LOAD_CREATE_PALETTE)
461 fop->m_createPaletteFromRgba = true;
462
463 // Does data file exist?
464 if (flags & FILE_LOAD_DATA_FILE) {
465 std::string dataFilename = base::replace_extension(filename, "aseprite-data");
466 if (base::is_file(dataFilename))
467 fop->m_dataFilename = dataFilename;
468 }
469
470done:;
471 return fop.release();
472}
473
474// static
475FileOp* FileOp::createSaveDocumentOperation(const Context* context,
476 const FileOpROI& roi,
477 const std::string& filename,
478 const std::string& filenameFormatArg,
479 const bool ignoreEmptyFrames)
480{
481 std::unique_ptr<FileOp> fop(
482 new FileOp(FileOpSave, const_cast<Context*>(context), nullptr));
483
484 // Document to save
485 fop->m_document = const_cast<Doc*>(roi.document());
486 fop->m_roi = roi;
487 fop->m_ignoreEmpty = ignoreEmptyFrames;
488
489 // Get the extension of the filename (in lower case)
490 LOG("FILE: Saving document \"%s\"\n", filename.c_str());
491
492 // Check for read-only attribute
493 if (base::has_readonly_attr(filename)) {
494 fop->setError("Error saving \"%s\" file, it's read-only",
495 filename.c_str());
496 return fop.release();
497 }
498
499 // Get the format through the extension of the filename
500 fop->m_format = FileFormatsManager::instance()->getFileFormat(
501 dio::detect_format_by_file_extension(filename));
502 if (!fop->m_format ||
503 !fop->m_format->support(FILE_SUPPORT_SAVE)) {
504 fop->setError("%s can't save \"%s\" file (\"%s\")\n", get_app_name(),
505 filename.c_str(), base::get_file_extension(filename).c_str());
506 return fop.release();
507 }
508
509 // Warnings
510 std::string warnings;
511 bool fatal = false;
512
513 // Check image type support
514 // TODO add support to automatically convert the image to a supported format
515 switch (fop->m_document->sprite()->pixelFormat()) {
516
517 case IMAGE_RGB:
518 if (!(fop->m_format->support(FILE_SUPPORT_RGB))) {
519 warnings += "<<- " + Strings::alerts_file_format_rgb_mode();
520 fatal = true;
521 }
522
523 if (!(fop->m_format->support(FILE_SUPPORT_RGBA)) &&
524 fop->m_document->sprite()->needAlpha()) {
525
526 warnings += "<<- " + Strings::alerts_file_format_alpha_channel();
527 }
528 break;
529
530 case IMAGE_GRAYSCALE:
531 if (!(fop->m_format->support(FILE_SUPPORT_GRAY))) {
532 warnings += "<<- " + Strings::alerts_file_format_grayscale_mode();
533 fatal = true;
534 }
535 if (!(fop->m_format->support(FILE_SUPPORT_GRAYA)) &&
536 fop->m_document->sprite()->needAlpha()) {
537
538 warnings += "<<- " + Strings::alerts_file_format_alpha_channel();
539 }
540 break;
541
542 case IMAGE_INDEXED:
543 if (!(fop->m_format->support(FILE_SUPPORT_INDEXED))) {
544 warnings += "<<- " + Strings::alerts_file_format_indexed_mode();
545 fatal = true;
546 }
547 break;
548 }
549
550 // Frames support
551 if (fop->m_roi.frames() > 1) {
552 if (!fop->m_format->support(FILE_SUPPORT_FRAMES) &&
553 !fop->m_format->support(FILE_SUPPORT_SEQUENCES)) {
554 warnings += "<<- " + Strings::alerts_file_format_frames();
555 }
556 }
557
558 // Layers support
559 if (fop->m_document->sprite()->root()->layersCount() > 1) {
560 if (!(fop->m_format->support(FILE_SUPPORT_LAYERS))) {
561 warnings += "<<- " + Strings::alerts_file_format_layers();
562 }
563 }
564
565 // Palettes support
566 if (fop->m_document->sprite()->getPalettes().size() > 1) {
567 if (!fop->m_format->support(FILE_SUPPORT_PALETTES) &&
568 !fop->m_format->support(FILE_SUPPORT_SEQUENCES)) {
569 warnings += "<<- " + Strings::alerts_file_format_palette_changes();
570 }
571 }
572
573 // Check frames support
574 if (!fop->m_document->sprite()->tags().empty()) {
575 if (!fop->m_format->support(FILE_SUPPORT_TAGS)) {
576 warnings += "<<- " + Strings::alerts_file_format_tags();
577 }
578 }
579
580 // Big palettes
581 if (!fop->m_format->support(FILE_SUPPORT_BIG_PALETTES)) {
582 for (const Palette* pal : fop->m_document->sprite()->getPalettes()) {
583 if (pal->size() > 256) {
584 warnings += "<<- Palettes with more than 256 colors";
585 break;
586 }
587 }
588 }
589
590 // Palette with alpha
591 if (!fop->m_format->support(FILE_SUPPORT_PALETTE_WITH_ALPHA)) {
592 if (!fop->m_format->support(FILE_SUPPORT_RGBA) ||
593 !fop->m_format->support(FILE_SUPPORT_INDEXED) ||
594 fop->document()->colorMode() == ColorMode::INDEXED) {
595 bool done = false;
596 for (const Palette* pal : fop->m_document->sprite()->getPalettes()) {
597 for (int c=0; c<pal->size(); ++c) {
598 if (rgba_geta(pal->getEntry(c)) < 255) {
599 warnings += "<<- Palette with alpha channel";
600 done = true;
601 break;
602 }
603 }
604 if (done)
605 break;
606 }
607 }
608 }
609
610 // Show the confirmation alert
611 if (!warnings.empty()) {
612#ifdef ENABLE_UI
613 // Interative
614 if (context && context->isUIAvailable()) {
615 int ret;
616
617 // If the error is fatal, we cannot ignore a no-op, we always
618 // show the alert dialog.
619 if (fatal) {
620 ui::Alert::show(
621 fmt::format(
622 Strings::alerts_file_format_doesnt_support_error(),
623 fop->m_format->name(),
624 warnings));
625 ret = 1;
626 }
627 else {
628 ret = OptionalAlert::show(
629 Preferences::instance().saveFile.showFileFormatDoesntSupportAlert,
630 1, // Yes is the default option when the alert dialog is disabled
631 fmt::format(
632 Strings::alerts_file_format_doesnt_support_warning(),
633 fop->m_format->name(),
634 warnings));
635 }
636
637 // Operation can't be done (by fatal error) or the user cancel
638 // the operation
639 if ((fatal) || (ret != 1))
640 return nullptr;
641 }
642 // No interactive & fatal error?
643 else
644#endif // ENABLE_UI
645 if (fatal) {
646 fop->setError(warnings.c_str());
647 return fop.release();
648 }
649 }
650
651 // Use the "sequence" interface.
652 if (fop->m_format->support(FILE_SUPPORT_SEQUENCES)) {
653 fop->prepareForSequence();
654
655 std::string fn = filename;
656 std::string fn_format = filenameFormatArg;
657 if (fn_format.empty()) {
658 fn_format = get_default_filename_format(
659 fn,
660 true, // With path
661 (fop->m_roi.frames() > 1), // Has frames
662 false, // Doesn't have layers
663 false); // Doesn't have tags
664 }
665
666 Sprite* spr = fop->m_document->sprite();
667 frame_t outputFrame = 0;
668
669 for (frame_t frame : fop->m_roi.selectedFrames()) {
670 Tag* innerTag = (fop->m_roi.tag() ? fop->m_roi.tag(): spr->tags().innerTag(frame));
671 Tag* outerTag = (fop->m_roi.tag() ? fop->m_roi.tag(): spr->tags().outerTag(frame));
672 FilenameInfo fnInfo;
673 fnInfo
674 .filename(fn)
675 .sliceName(fop->m_roi.slice() ? fop->m_roi.slice()->name(): "")
676 .innerTagName(innerTag ? innerTag->name(): "")
677 .outerTagName(outerTag ? outerTag->name(): "")
678 .frame(outputFrame)
679 .tagFrame(innerTag ? frame - innerTag->fromFrame():
680 outputFrame)
681 .duration(spr->frameDuration(frame));
682
683 fop->m_seq.filename_list.push_back(
684 filename_formatter(fn_format, fnInfo));
685
686 ++outputFrame;
687 }
688
689#ifdef ENABLE_UI
690 if (context && context->isUIAvailable() &&
691 fop->m_seq.filename_list.size() > 1 &&
692 OptionalAlert::show(
693 Preferences::instance().saveFile.showExportAnimationInSequenceAlert,
694 1,
695 fmt::format(
696 Strings::alerts_export_animation_in_sequence(),
697 int(fop->m_seq.filename_list.size()),
698 base::get_file_name(fop->m_seq.filename_list[0]),
699 base::get_file_name(fop->m_seq.filename_list[1]))) != 1) {
700 return nullptr;
701 }
702#endif // ENABLE_UI
703 }
704 else
705 fop->m_filename = filename;
706
707 // Configure output format?
708 if (fop->m_format->support(FILE_SUPPORT_GET_FORMAT_OPTIONS)) {
709 auto opts = fop->m_format->askUserForFormatOptions(fop.get());
710
711 // Does the user cancelled the operation?
712 if (!opts)
713 return nullptr;
714
715 fop->m_formatOptions = opts;
716 fop->m_document->setFormatOptions(opts);
717 }
718
719 // Does data file exist?
720 std::string dataFilename = base::replace_extension(filename, "aseprite-data");
721 if (base::is_file(dataFilename))
722 fop->m_dataFilename = dataFilename;
723
724 return fop.release();
725}
726
727// static
728bool FileOp::checkIfFormatSupportResizeOnTheFly(const std::string& filename)
729{
730 // Get the format through the extension of the filename
731 FileFormat* fileFormat =
732 FileFormatsManager::instance()->getFileFormat(
733 dio::detect_format_by_file_extension(filename));
734
735 return (fileFormat &&
736 fileFormat->support(FILE_ENCODE_ABSTRACT_IMAGE));
737}
738
739// Executes the file operation: loads or saves the sprite.
740//
741// It can be called from a different thread of the one used
742// by FileOp::createLoadDocumentOperation() or createSaveDocumentOperation().
743//
744// After this function you must to mark the FileOp as "done" calling
745// FileOp::done() function.
746//
747// TODO refactor this code
748void FileOp::operate(IFileOpProgress* progress)
749{
750 ASSERT(!isDone());
751
752 m_progressInterface = progress;
753
754 // Load //////////////////////////////////////////////////////////////////////
755 if (m_type == FileOpLoad &&
756 m_format != NULL &&
757 m_format->support(FILE_SUPPORT_LOAD)) {
758 // Load a sequence
759 if (isSequence()) {
760 // Default palette
761 m_seq.palette->makeBlack();
762
763 // Load the sequence
764 frame_t frames(m_seq.filename_list.size());
765 frame_t frame(0);
766 Image* old_image = nullptr;
767 gfx::Size canvasSize(0, 0);
768
769 // TODO setPalette for each frame???
770 auto add_image = [&]() {
771 canvasSize |= m_seq.image->size();
772
773 m_seq.last_cel->data()->setImage(m_seq.image,
774 m_seq.layer);
775 m_seq.layer->addCel(m_seq.last_cel);
776
777 if (m_document->sprite()->palette(frame)
778 ->countDiff(m_seq.palette, NULL, NULL) > 0) {
779 m_seq.palette->setFrame(frame);
780 m_document->sprite()->setPalette(m_seq.palette, true);
781 }
782
783 old_image = m_seq.image.get();
784 m_seq.image.reset();
785 m_seq.last_cel = NULL;
786 };
787
788 m_seq.has_alpha = false;
789 m_seq.progress_offset = 0.0f;
790 m_seq.progress_fraction = 1.0f / (double)frames;
791
792 auto it = m_seq.filename_list.begin(),
793 end = m_seq.filename_list.end();
794 for (; it != end; ++it) {
795 m_filename = it->c_str();
796
797 // Call the "load" procedure to read the first bitmap.
798 bool loadres = m_format->load(this);
799 if (!loadres) {
800 setError("Error loading frame %d from file \"%s\"\n",
801 frame+1, m_filename.c_str());
802 }
803
804 // For the first frame...
805 if (!old_image) {
806 // Error reading the first frame
807 if (!loadres || !m_document || !m_seq.last_cel) {
808 m_seq.image.reset();
809 delete m_seq.last_cel;
810 delete m_document;
811 m_document = nullptr;
812 break;
813 }
814 // Read ok
815 else {
816 // Add the keyframe
817 add_image();
818 }
819 }
820 // For other frames
821 else {
822 // All done (or maybe not enough memory)
823 if (!loadres || !m_seq.last_cel) {
824 m_seq.image.reset();
825 delete m_seq.last_cel;
826 break;
827 }
828
829 // Compare the old frame with the new one
830#if USE_LINK // TODO this should be configurable through a check-box
831 if (count_diff_between_images(old_image, m_seq.image)) {
832 add_image();
833 }
834 // We don't need this image
835 else {
836 delete m_seq.image;
837
838 // But add a link frame
839 m_seq.last_cel->image = image_index;
840 layer_add_frame(m_seq.layer, m_seq.last_cel);
841
842 m_seq.last_image = NULL;
843 m_seq.last_cel = NULL;
844 }
845#else
846 add_image();
847#endif
848 }
849
850 m_document->sprite()->setFrameDuration(frame, m_seq.duration);
851
852 ++frame;
853 m_seq.progress_offset += m_seq.progress_fraction;
854 }
855 m_filename = *m_seq.filename_list.begin();
856
857 // Final setup
858 if (m_document) {
859 // Configure the layer as the 'Background'
860 if (!m_seq.has_alpha)
861 m_seq.layer->configureAsBackground();
862
863 // Set the final canvas size (as the bigger loaded
864 // frame/image).
865 m_document->sprite()->setSize(canvasSize.w,
866 canvasSize.h);
867
868 // Set the frames range
869 m_document->sprite()->setTotalFrames(frame);
870
871 // Sets special options from the specific format (e.g. BMP
872 // file can contain the number of bits per pixel).
873 m_document->setFormatOptions(m_formatOptions);
874 }
875 }
876 // Direct load from one file.
877 else {
878 // Call the "load" procedure.
879 if (!m_format->load(this)) {
880 setError("Error loading sprite from file \"%s\"\n",
881 m_filename.c_str());
882 }
883 }
884
885 // Load special data from .aseprite-data file
886 if (m_document &&
887 m_document->sprite() &&
888 !m_dataFilename.empty()) {
889 try {
890 load_aseprite_data_file(m_dataFilename,
891 m_document,
892 m_config.defaultSliceColor);
893 }
894 catch (const std::exception& ex) {
895 setError("Error loading data file: %s\n", ex.what());
896 }
897 }
898 }
899 // Save //////////////////////////////////////////////////////////////////////
900 else if (m_type == FileOpSave &&
901 m_format != NULL &&
902 m_format->support(FILE_SUPPORT_SAVE)) {
903#ifdef ENABLE_SAVE
904
905#if defined(ENABLE_TRIAL_MODE)
906 DRM_INVALID{
907 setError(
908 fmt::format("Save operation is not supported in trial version, activate this Aseprite first.\n"
909 "Go to {} and get a license key to upgrade.",
910 get_app_download_url()).c_str());
911 }
912#endif
913
914 // Save a sequence
915 if (isSequence()) {
916 ASSERT(m_format->support(FILE_SUPPORT_SEQUENCES));
917
918 Sprite* sprite = m_document->sprite();
919
920 // Create a temporary bitmap
921 m_seq.image.reset(Image::create(sprite->pixelFormat(),
922 sprite->width(),
923 sprite->height()));
924
925 m_seq.progress_offset = 0.0f;
926 m_seq.progress_fraction = 1.0f / (double)sprite->totalFrames();
927
928 // For each frame in the sprite.
929 render::Render render;
930 render.setNewBlend(m_config.newBlend);
931
932 frame_t outputFrame = 0;
933 for (frame_t frame : m_roi.selectedFrames()) {
934 gfx::Rect bounds;
935
936 // Export bounds of specific slice
937 if (m_roi.slice()) {
938 const SliceKey* key = m_roi.slice()->getByFrame(frame);
939 if (!key || key->isEmpty())
940 continue; // Skip frame because there is no slice key
941
942 bounds = key->bounds();
943 }
944 // Export specific bounds
945 else if (!m_roi.bounds().isEmpty()) {
946 bounds = m_roi.bounds();
947 }
948
949 // Draw the "frame" in "m_seq.image" with the given bounds
950 // (bounds can be the selection bounds or a slice key bounds)
951 if (!bounds.isEmpty()) {
952 if (m_abstractImage)
953 m_abstractImage->setSpecSize(bounds.size());
954
955 m_seq.image.reset(
956 Image::create(sprite->pixelFormat(),
957 bounds.w,
958 bounds.h));
959
960 render.renderSprite(
961 m_seq.image.get(), sprite, frame,
962 gfx::Clip(gfx::Point(0, 0), bounds));
963 }
964 else {
965 render.renderSprite(m_seq.image.get(), sprite, frame);
966 }
967
968 bool save = true;
969
970 // Check if we have to ignore empty frames
971 if (m_ignoreEmpty &&
972 !sprite->isOpaque() &&
973 doc::is_empty_image(m_seq.image.get())) {
974 save = false;
975 }
976
977 if (save) {
978 // Setup the palette.
979 sprite->palette(frame)->copyColorsTo(m_seq.palette);
980
981 // Setup the filename to be used.
982 m_filename = m_seq.filename_list[outputFrame];
983
984 // Make directories
985 {
986 std::string dir = base::get_file_path(m_filename);
987 try {
988 if (!base::is_directory(dir))
989 base::make_all_directories(dir);
990 }
991 catch (const std::exception& ex) {
992 // Ignore errors and make the delegate fail
993 setError("Error creating directory \"%s\"\n%s",
994 dir.c_str(), ex.what());
995 }
996 }
997
998 // Call the "save" procedure... did it fail?
999 if (!m_format->save(this)) {
1000 setError("Error saving frame %d in the file \"%s\"\n",
1001 outputFrame+1, m_filename.c_str());
1002 break;
1003 }
1004 }
1005
1006 m_seq.progress_offset += m_seq.progress_fraction;
1007 ++outputFrame;
1008 }
1009
1010 m_filename = *m_seq.filename_list.begin();
1011
1012 // Destroy the image
1013 m_seq.image.reset();
1014 }
1015 // Direct save to a file.
1016 else {
1017 // Call the "save" procedure.
1018 if (!m_format->save(this)) {
1019 setError("Error saving the sprite in the file \"%s\"\n",
1020 m_filename.c_str());
1021 }
1022 }
1023
1024 // Save special data from .aseprite-data file
1025 if (m_document &&
1026 m_document->sprite() &&
1027 !hasError() &&
1028 !m_dataFilename.empty()) {
1029 try {
1030 save_aseprite_data_file(m_dataFilename, m_document);
1031 }
1032 catch (const std::exception& ex) {
1033 setError("Error loading data file: %s\n", ex.what());
1034 }
1035 }
1036#else
1037 setError(
1038 fmt::format("Save operation is not supported in trial version.\n"
1039 "Go to {} and get the full-version.",
1040 get_app_download_url()).c_str());
1041#endif
1042 }
1043
1044 // Progress = 100%
1045 setProgress(1.0f);
1046}
1047
1048// After mark the 'fop' as 'done' you must to free it calling fop_free().
1049void FileOp::done()
1050{
1051 // Finally done.
1052 std::lock_guard lock(m_mutex);
1053 m_done = true;
1054}
1055
1056void FileOp::stop()
1057{
1058 std::lock_guard lock(m_mutex);
1059 if (!m_done)
1060 m_stop = true;
1061}
1062
1063FileOp::~FileOp()
1064{
1065 delete m_seq.palette;
1066}
1067
1068void FileOp::createDocument(Sprite* spr)
1069{
1070 // spr can be NULL if the sprite is set in onPostLoad() then
1071
1072 ASSERT(m_document == NULL);
1073 m_document = new Doc(spr);
1074}
1075
1076void FileOp::postLoad()
1077{
1078 if (m_document == NULL)
1079 return;
1080
1081 // Set the filename.
1082 std::string fn;
1083 if (isSequence())
1084 fn = m_seq.filename_list.begin()->c_str();
1085 else
1086 fn = m_filename.c_str();
1087 m_document->setFilename(fn);
1088
1089 bool result = m_format->postLoad(this);
1090 if (!result) {
1091 // Destroy the document
1092 delete m_document;
1093 m_document = nullptr;
1094 return;
1095 }
1096
1097 Sprite* sprite = m_document->sprite();
1098 if (sprite) {
1099 // Creates a suitable palette for RGB images
1100 if (m_createPaletteFromRgba &&
1101 sprite->pixelFormat() == IMAGE_RGB &&
1102 sprite->getPalettes().size() <= 1 &&
1103 sprite->palette(frame_t(0))->isBlack()) {
1104 std::shared_ptr<Palette> palette(
1105 render::create_palette_from_sprite(
1106 sprite, frame_t(0), sprite->lastFrame(), true,
1107 nullptr, nullptr, m_config.newBlend,
1108 m_config.rgbMapAlgorithm));
1109
1110 sprite->resetPalettes();
1111 sprite->setPalette(palette.get(), false);
1112 }
1113 }
1114
1115 // What to do with the sprite color profile?
1116 gfx::ColorSpaceRef spriteCS = sprite->colorSpace();
1117 app::gen::ColorProfileBehavior behavior =
1118 app::gen::ColorProfileBehavior::DISABLE;
1119
1120 if (m_config.preserveColorProfile) {
1121 // Embedded color profile
1122 if (this->hasEmbeddedColorProfile()) {
1123 behavior = m_config.filesWithProfile;
1124 if (behavior == app::gen::ColorProfileBehavior::ASK) {
1125#ifdef ENABLE_UI
1126 if (m_context && m_context->isUIAvailable()) {
1127 app::gen::AskForColorProfile window;
1128 window.spriteWithoutProfile()->setVisible(false);
1129 window.openWindowInForeground();
1130 auto c = window.closer();
1131 if (c == window.embedded())
1132 behavior = app::gen::ColorProfileBehavior::EMBEDDED;
1133 else if (c == window.convert())
1134 behavior = app::gen::ColorProfileBehavior::CONVERT;
1135 else if (c == window.assign())
1136 behavior = app::gen::ColorProfileBehavior::ASSIGN;
1137 else
1138 behavior = app::gen::ColorProfileBehavior::DISABLE;
1139 }
1140 else
1141#endif // ENABLE_UI
1142 {
1143 behavior = app::gen::ColorProfileBehavior::EMBEDDED;
1144 }
1145 }
1146 }
1147 // Missing color space
1148 else {
1149 behavior = m_config.missingProfile;
1150 if (behavior == app::gen::ColorProfileBehavior::ASK) {
1151#ifdef ENABLE_UI
1152 if (m_context && m_context->isUIAvailable()) {
1153 app::gen::AskForColorProfile window;
1154 window.spriteWithProfile()->setVisible(false);
1155 window.embedded()->setVisible(false);
1156 window.convert()->setVisible(false);
1157 window.openWindowInForeground();
1158 if (window.closer() == window.assign()) {
1159 behavior = app::gen::ColorProfileBehavior::ASSIGN;
1160 }
1161 else {
1162 behavior = app::gen::ColorProfileBehavior::DISABLE;
1163 }
1164 }
1165 else
1166#endif // ENABLE_UI
1167 {
1168 behavior = app::gen::ColorProfileBehavior::ASSIGN;
1169 }
1170 }
1171 }
1172 }
1173
1174 switch (behavior) {
1175
1176 case app::gen::ColorProfileBehavior::DISABLE:
1177 sprite->setColorSpace(gfx::ColorSpace::MakeNone());
1178 m_document->notifyColorSpaceChanged();
1179 break;
1180
1181 case app::gen::ColorProfileBehavior::EMBEDDED:
1182 // Do nothing, just keep the current sprite's color sprite
1183 break;
1184
1185 case app::gen::ColorProfileBehavior::CONVERT: {
1186 // Convert to the working color profile
1187 auto gfxCS = m_config.workingCS;
1188 if (!gfxCS->nearlyEqual(*spriteCS))
1189 cmd::convert_color_profile(sprite, gfxCS);
1190 break;
1191 }
1192
1193 case app::gen::ColorProfileBehavior::ASSIGN: {
1194 // Convert to the working color profile
1195 auto gfxCS = m_config.workingCS;
1196 sprite->setColorSpace(gfxCS);
1197 m_document->notifyColorSpaceChanged();
1198 break;
1199 }
1200 }
1201
1202 m_document->markAsSaved();
1203}
1204
1205void FileOp::setLoadedFormatOptions(const FormatOptionsPtr& opts)
1206{
1207 // This assert can fail when we load a sequence of files.
1208 // TODO what we should do, keep the first or the latest format options?
1209 //ASSERT(!m_formatOptions);
1210 m_formatOptions = opts;
1211}
1212
1213void FileOp::sequenceSetNColors(int ncolors)
1214{
1215 m_seq.palette->resize(ncolors);
1216}
1217
1218int FileOp::sequenceGetNColors() const
1219{
1220 return m_seq.palette->size();
1221}
1222
1223void FileOp::sequenceSetColor(int index, int r, int g, int b)
1224{
1225 m_seq.palette->setEntry(index, rgba(r, g, b, 255));
1226}
1227
1228void FileOp::sequenceGetColor(int index, int* r, int* g, int* b) const
1229{
1230 uint32_t c;
1231
1232 ASSERT(index >= 0);
1233 if (index >= 0 && index < m_seq.palette->size())
1234 c = m_seq.palette->getEntry(index);
1235 else
1236 c = rgba(0, 0, 0, 255); // Black color
1237
1238 *r = rgba_getr(c);
1239 *g = rgba_getg(c);
1240 *b = rgba_getb(c);
1241}
1242
1243void FileOp::sequenceSetAlpha(int index, int a)
1244{
1245 int c = m_seq.palette->getEntry(index);
1246 int r = rgba_getr(c);
1247 int g = rgba_getg(c);
1248 int b = rgba_getb(c);
1249
1250 m_seq.palette->setEntry(index, rgba(r, g, b, a));
1251}
1252
1253void FileOp::sequenceGetAlpha(int index, int* a) const
1254{
1255 ASSERT(index >= 0);
1256 if (index >= 0 && index < m_seq.palette->size())
1257 *a = rgba_geta(m_seq.palette->getEntry(index));
1258 else
1259 *a = 0;
1260}
1261
1262ImageRef FileOp::sequenceImage(PixelFormat pixelFormat, int w, int h)
1263{
1264 Sprite* sprite;
1265
1266 // Create the image
1267 if (!m_document) {
1268 sprite = new Sprite(ImageSpec((ColorMode)pixelFormat, w, h), 256);
1269 try {
1270 LayerImage* layer = new LayerImage(sprite);
1271
1272 // Add the layer
1273 sprite->root()->addLayer(layer);
1274
1275 // Done
1276 createDocument(sprite);
1277 m_seq.layer = layer;
1278 }
1279 catch (...) {
1280 delete sprite;
1281 throw;
1282 }
1283 }
1284 else {
1285 sprite = m_document->sprite();
1286
1287 if (sprite->pixelFormat() != pixelFormat)
1288 return nullptr;
1289 }
1290
1291 if (m_seq.last_cel) {
1292 setError("Error: called two times FileOp::sequenceImage()\n");
1293 return nullptr;
1294 }
1295
1296 // Create a bitmap
1297 m_seq.image.reset(Image::create(pixelFormat, w, h));
1298 m_seq.last_cel = new Cel(m_seq.frame++, ImageRef(nullptr));
1299
1300 return m_seq.image;
1301}
1302
1303void FileOp::makeAbstractImage()
1304{
1305 ASSERT(m_format->support(FILE_ENCODE_ABSTRACT_IMAGE));
1306 if (!m_abstractImage)
1307 m_abstractImage = std::make_unique<FileAbstractImageImpl>(this);
1308}
1309
1310FileAbstractImage* FileOp::abstractImage()
1311{
1312 ASSERT(m_format->support(FILE_ENCODE_ABSTRACT_IMAGE));
1313
1314 makeAbstractImage();
1315
1316 // Use sequenceImage() to fill the current image
1317 if (m_format->support(FILE_SUPPORT_SEQUENCES))
1318 m_abstractImage->setUnscaledImage(m_seq.frame, sequenceImage());
1319
1320 return m_abstractImage.get();
1321}
1322
1323void FileOp::setOnTheFlyScale(const gfx::PointF& scale)
1324{
1325 makeAbstractImage();
1326 m_abstractImage->setScale(scale);
1327}
1328
1329void FileOp::setError(const char *format, ...)
1330{
1331 char buf_error[4096]; // TODO possible stack overflow
1332 va_list ap;
1333 va_start(ap, format);
1334 vsnprintf(buf_error, sizeof(buf_error), format, ap);
1335 va_end(ap);
1336
1337 // Concatenate the new error
1338 {
1339 std::lock_guard lock(m_mutex);
1340 // Add a newline char automatically if it's needed
1341 if (!m_error.empty() && m_error.back() != '\n')
1342 m_error.push_back('\n');
1343 m_error += buf_error;
1344 }
1345}
1346
1347void FileOp::setProgress(double progress)
1348{
1349 std::lock_guard lock(m_mutex);
1350
1351 if (isSequence()) {
1352 m_progress =
1353 m_seq.progress_offset +
1354 m_seq.progress_fraction*progress;
1355 }
1356 else {
1357 m_progress = progress;
1358 }
1359
1360 if (m_progressInterface)
1361 m_progressInterface->ackFileOpProgress(progress);
1362}
1363
1364void FileOp::getFilenameList(base::paths& output) const
1365{
1366 if (isSequence()) {
1367 output = m_seq.filename_list;
1368 }
1369 else {
1370 output.push_back(m_filename);
1371 }
1372}
1373
1374double FileOp::progress() const
1375{
1376 double progress;
1377 {
1378 std::lock_guard lock(m_mutex);
1379 progress = m_progress;
1380 }
1381 return progress;
1382}
1383
1384// Returns true when the file operation has finished, this means, when
1385// the FileOp::operate() routine ends.
1386bool FileOp::isDone() const
1387{
1388 bool done;
1389 {
1390 std::lock_guard lock(m_mutex);
1391 done = m_done;
1392 }
1393 return done;
1394}
1395
1396bool FileOp::isStop() const
1397{
1398 bool stop;
1399 {
1400 std::scoped_lock lock(m_mutex);
1401 stop = m_stop;
1402 }
1403 return stop;
1404}
1405
1406FileOp::FileOp(FileOpType type,
1407 Context* context,
1408 const FileOpConfig* config)
1409 : m_type(type)
1410 , m_format(nullptr)
1411 , m_context(context)
1412 , m_document(nullptr)
1413 , m_progress(0.0)
1414 , m_progressInterface(nullptr)
1415 , m_done(false)
1416 , m_stop(false)
1417 , m_oneframe(false)
1418 , m_createPaletteFromRgba(false)
1419 , m_ignoreEmpty(false)
1420 , m_embeddedColorProfile(false)
1421 , m_embeddedGridBounds(false)
1422{
1423 if (config)
1424 m_config = *config;
1425 else if (ui::is_ui_thread())
1426 m_config.fillFromPreferences();
1427 else {
1428 LOG(VERBOSE, "FILE: Using a file operation with default configuration\n");
1429 }
1430
1431 m_seq.palette = nullptr;
1432 m_seq.image.reset();
1433 m_seq.progress_offset = 0.0f;
1434 m_seq.progress_fraction = 0.0f;
1435 m_seq.frame = frame_t(0);
1436 m_seq.layer = nullptr;
1437 m_seq.last_cel = nullptr;
1438 m_seq.duration = 100;
1439 m_seq.flags = 0;
1440}
1441
1442void FileOp::prepareForSequence()
1443{
1444 m_seq.palette = new Palette(frame_t(0), 256);
1445 m_formatOptions.reset();
1446}
1447
1448} // namespace app
1449