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 | |
52 | namespace app { |
53 | |
54 | using namespace base; |
55 | |
56 | class FileOp::FileAbstractImageImpl : public FileAbstractImage { |
57 | public: |
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 | |
169 | private: |
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 | |
179 | base::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 | |
189 | base::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 | |
201 | Doc* 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 | |
229 | int 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 | |
252 | bool 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 | |
262 | FileOpROI::FileOpROI() |
263 | : m_document(nullptr) |
264 | , m_slice(nullptr) |
265 | , m_tag(nullptr) |
266 | { |
267 | } |
268 | |
269 | FileOpROI::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 |
306 | FileOp* 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 | |
470 | done:; |
471 | return fop.release(); |
472 | } |
473 | |
474 | // static |
475 | FileOp* 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 |
728 | bool 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 |
748 | void 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(). |
1049 | void FileOp::done() |
1050 | { |
1051 | // Finally done. |
1052 | std::lock_guard lock(m_mutex); |
1053 | m_done = true; |
1054 | } |
1055 | |
1056 | void FileOp::stop() |
1057 | { |
1058 | std::lock_guard lock(m_mutex); |
1059 | if (!m_done) |
1060 | m_stop = true; |
1061 | } |
1062 | |
1063 | FileOp::~FileOp() |
1064 | { |
1065 | delete m_seq.palette; |
1066 | } |
1067 | |
1068 | void 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 | |
1076 | void 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 | |
1205 | void 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 | |
1213 | void FileOp::sequenceSetNColors(int ncolors) |
1214 | { |
1215 | m_seq.palette->resize(ncolors); |
1216 | } |
1217 | |
1218 | int FileOp::sequenceGetNColors() const |
1219 | { |
1220 | return m_seq.palette->size(); |
1221 | } |
1222 | |
1223 | void FileOp::sequenceSetColor(int index, int r, int g, int b) |
1224 | { |
1225 | m_seq.palette->setEntry(index, rgba(r, g, b, 255)); |
1226 | } |
1227 | |
1228 | void 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 | |
1243 | void 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 | |
1253 | void 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 | |
1262 | ImageRef 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 | |
1303 | void 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 | |
1310 | FileAbstractImage* 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 | |
1323 | void FileOp::setOnTheFlyScale(const gfx::PointF& scale) |
1324 | { |
1325 | makeAbstractImage(); |
1326 | m_abstractImage->setScale(scale); |
1327 | } |
1328 | |
1329 | void 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 | |
1347 | void 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 | |
1364 | void 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 | |
1374 | double 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. |
1386 | bool 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 | |
1396 | bool 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 | |
1406 | FileOp::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 | |
1442 | void FileOp::prepareForSequence() |
1443 | { |
1444 | m_seq.palette = new Palette(frame_t(0), 256); |
1445 | m_formatOptions.reset(); |
1446 | } |
1447 | |
1448 | } // namespace app |
1449 | |