1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2015-2018 David Capello
4// Copyright (C) 2015 Gabriel Rauter
5//
6// This program is distributed under the terms of
7// the End-User License Agreement for Aseprite.
8
9#ifdef HAVE_CONFIG_H
10#include "config.h"
11#endif
12
13#include "app/app.h"
14#include "app/console.h"
15#include "app/context.h"
16#include "app/doc.h"
17#include "app/file/file.h"
18#include "app/file/file_format.h"
19#include "app/file/format_options.h"
20#include "app/file/webp_options.h"
21#include "app/ini_file.h"
22#include "app/pref/preferences.h"
23#include "base/convert_to.h"
24#include "base/file_handle.h"
25#include "doc/doc.h"
26#include "ui/manager.h"
27
28#include "webp_options.xml.h"
29
30#include <cstdio>
31#include <cstdlib>
32#include <algorithm>
33#include <map>
34
35#include <webp/demux.h>
36#include <webp/mux.h>
37
38namespace app {
39
40using namespace base;
41
42class WebPFormat : public FileFormat {
43
44 const char* onGetName() const override {
45 return "webp";
46 }
47
48 void onGetExtensions(base::paths& exts) const override {
49 exts.push_back("webp");
50 }
51
52 dio::FileFormat onGetDioFormat() const override {
53 return dio::FileFormat::WEBP_ANIMATION;
54 }
55
56 int onGetFlags() const override {
57 return
58 FILE_SUPPORT_LOAD |
59 FILE_SUPPORT_SAVE |
60 FILE_SUPPORT_RGB |
61 FILE_SUPPORT_RGBA |
62 FILE_SUPPORT_FRAMES |
63 FILE_SUPPORT_GET_FORMAT_OPTIONS |
64 FILE_ENCODE_ABSTRACT_IMAGE;
65 }
66
67 bool onLoad(FileOp* fop) override;
68#ifdef ENABLE_SAVE
69 bool onSave(FileOp* fop) override;
70#endif
71 FormatOptionsPtr onAskUserForFormatOptions(FileOp* fop) override;
72};
73
74FileFormat* CreateWebPFormat()
75{
76 return new WebPFormat;
77}
78
79const char* getDecoderErrorMessage(VP8StatusCode statusCode)
80{
81 switch (statusCode) {
82 case VP8_STATUS_OK: return ""; break;
83 case VP8_STATUS_OUT_OF_MEMORY: return "out of memory"; break;
84 case VP8_STATUS_INVALID_PARAM: return "invalid parameters"; break;
85 case VP8_STATUS_BITSTREAM_ERROR: return "bitstream error"; break;
86 case VP8_STATUS_UNSUPPORTED_FEATURE: return "unsupported feature"; break;
87 case VP8_STATUS_SUSPENDED: return "suspended"; break;
88 case VP8_STATUS_USER_ABORT: return "user aborted"; break;
89 case VP8_STATUS_NOT_ENOUGH_DATA: return "not enough data"; break;
90 default: return "unknown error"; break;
91 }
92}
93
94bool WebPFormat::onLoad(FileOp* fop)
95{
96 FileHandle handle(open_file_with_exception(fop->filename(), "rb"));
97 FILE* fp = handle.get();
98
99 long len = 0;
100 if (fseek(fp, 0, SEEK_END) == 0) {
101 len = ftell(fp);
102 fseek(fp, 0, SEEK_SET);
103 }
104
105 if (len < 4) {
106 fop->setError("The specified file is not a WebP file\n");
107 return false;
108 }
109
110 std::vector<uint8_t> buf(len);
111 if (fread(&buf[0], 1, buf.size(), fp) != buf.size()) {
112 fop->setError("Error moving the whole WebP file to memory\n");
113 return false;
114 }
115
116 WebPData webp_data;
117 WebPDataInit(&webp_data);
118 webp_data.bytes = &buf[0];
119 webp_data.size = buf.size();
120
121 WebPAnimDecoderOptions dec_options;
122 WebPAnimDecoderOptionsInit(&dec_options);
123 dec_options.color_mode = MODE_RGBA;
124
125 WebPAnimDecoder* dec = WebPAnimDecoderNew(&webp_data, &dec_options);
126 if (dec == nullptr) {
127 fop->setError("Error parsing WebP image\n");
128 return false;
129 }
130
131 WebPAnimInfo anim_info;
132 if (!WebPAnimDecoderGetInfo(dec, &anim_info)) {
133 fop->setError("Error getting global info about the WebP animation\n");
134 return false;
135 }
136
137 WebPDecoderConfig config;
138 WebPInitDecoderConfig(&config);
139 if (WebPGetFeatures(webp_data.bytes, webp_data.size, &config.input)) {
140 if (!fop->formatOptions()) {
141 auto opts = std::make_shared<WebPOptions>();
142 WebPOptions::Type type = WebPOptions::Simple;
143 switch (config.input.format) {
144 case 0: type = WebPOptions::Simple; break;
145 case 1: type = WebPOptions::Lossy; break;
146 case 2: type = WebPOptions::Lossless; break;
147 }
148 opts->setType(type);
149 fop->setLoadedFormatOptions(opts);
150 }
151 }
152 else {
153 config.input.has_alpha = false;
154 }
155
156 const int w = anim_info.canvas_width;
157 const int h = anim_info.canvas_height;
158
159 Sprite* sprite = new Sprite(ImageSpec(ColorMode::RGB, w, h), 256);
160 LayerImage* layer = new LayerImage(sprite);
161 sprite->root()->addLayer(layer);
162 sprite->setTotalFrames(anim_info.frame_count);
163
164 for (frame_t f=0; f<anim_info.frame_count; ++f) {
165 ImageRef image(Image::create(IMAGE_RGB, w, h));
166 Cel* cel = new Cel(f, image);
167 layer->addCel(cel);
168 }
169
170 bool has_alpha = config.input.has_alpha;
171 frame_t f = 0;
172 int prev_timestamp = 0;
173 while (WebPAnimDecoderHasMoreFrames(dec)) {
174 uint8_t* frame_rgba;
175 int frame_timestamp = 0;
176 if (!WebPAnimDecoderGetNext(dec, &frame_rgba, &frame_timestamp)) {
177 fop->setError("Error loading WebP frame\n");
178 return false;
179 }
180
181 Cel* cel = layer->cel(f);
182 if (cel) {
183 memcpy(cel->image()->getPixelAddress(0, 0),
184 frame_rgba, h*w*sizeof(uint32_t));
185
186 if (!has_alpha) {
187 const uint32_t* src = (const uint32_t*)frame_rgba;
188 const uint32_t* src_end = src + w*h;
189 while (src < src_end) {
190 const uint8_t alpha = (*src >> 24) & 0xff;
191 if (alpha < 255) {
192 has_alpha = true;
193 break;
194 }
195 ++src;
196 }
197 }
198 }
199
200 sprite->setFrameDuration(f, frame_timestamp - prev_timestamp);
201
202 prev_timestamp = frame_timestamp;
203 fop->setProgress(double(f) / double(anim_info.frame_count));
204 if (fop->isStop())
205 break;
206
207 ++f;
208 }
209 WebPAnimDecoderReset(dec);
210
211 if (!has_alpha)
212 layer->configureAsBackground();
213
214 WebPAnimDecoderDelete(dec);
215
216 // Don't use WebPDataClear because webp_data use a std::vector<> data.
217 //WebPDataClear(&webp_data);
218
219 if (fop->isStop())
220 return false;
221
222 fop->createDocument(sprite);
223 return true;
224}
225
226#ifdef ENABLE_SAVE
227
228struct WriterData {
229 FILE* fp;
230 FileOp* fop;
231 frame_t f, n;
232 double progress;
233
234 WriterData(FILE* fp, FileOp* fop, frame_t f, frame_t n, double progress)
235 : fp(fp), fop(fop), f(f), n(n), progress(progress) { }
236};
237
238static int progress_report(int percent, const WebPPicture* pic)
239{
240 auto wd = (WriterData*)pic->user_data;
241 FileOp* fop = wd->fop;
242
243 double newProgress = (double(wd->f) + double(percent)/100.0) / double(wd->n);
244 wd->progress = std::max(wd->progress, newProgress);
245 wd->progress = std::clamp(wd->progress, 0.0, 1.0);
246
247 fop->setProgress(wd->progress);
248 if (fop->isStop())
249 return false;
250 else
251 return true;
252}
253
254bool WebPFormat::onSave(FileOp* fop)
255{
256 FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb"));
257 FILE* fp = handle.get();
258
259 const FileAbstractImage* sprite = fop->abstractImage();
260 const doc::frame_t totalFrames = sprite->frames();
261 const int w = sprite->width();
262 const int h = sprite->height();
263
264 if (w > WEBP_MAX_DIMENSION ||
265 h > WEBP_MAX_DIMENSION) {
266 fop->setError("WebP format cannot store %dx%d images. The maximum allowed size is %dx%d\n",
267 w, h, WEBP_MAX_DIMENSION, WEBP_MAX_DIMENSION);
268 return false;
269 }
270
271 auto opts = fop->formatOptionsForSaving<WebPOptions>();
272 WebPConfig config;
273 WebPConfigInit(&config);
274
275 switch (opts->type()) {
276
277 case WebPOptions::Simple:
278 case WebPOptions::Lossless:
279 if (!WebPConfigLosslessPreset(&config,
280 opts->compression())) {
281 fop->setError("Error in WebP configuration\n");
282 return false;
283 }
284 config.image_hint = opts->imageHint();
285 break;
286
287 case WebPOptions::Lossy:
288 if (!WebPConfigPreset(&config,
289 opts->imagePreset(),
290 static_cast<float>(opts->quality()))) {
291 fop->setError("Error in WebP configuration preset\n");
292 return false;
293 }
294 break;
295 }
296
297 WebPAnimEncoderOptions enc_options;
298 WebPAnimEncoderOptionsInit(&enc_options);
299 enc_options.anim_params.loop_count =
300 (opts->loop() ? 0: // 0 = infinite
301 1); // 1 = loop once
302
303 ImageRef image(Image::create(IMAGE_RGB, w, h));
304
305 WriterData wd(fp, fop, 0, totalFrames, 0.0);
306 WebPPicture pic;
307 WebPPictureInit(&pic);
308 pic.width = w;
309 pic.height = h;
310 pic.use_argb = true;
311 pic.argb = (uint32_t*)image->getPixelAddress(0, 0);
312 pic.argb_stride = w;
313 pic.user_data = &wd;
314 pic.progress_hook = progress_report;
315
316 WebPAnimEncoder* enc = WebPAnimEncoderNew(w, h, &enc_options);
317 int timestamp_ms = 0;
318 for (frame_t f=0; f<totalFrames; ++f) {
319 // Render the frame in the bitmap
320 clear_image(image.get(), image->maskColor());
321 sprite->renderFrame(f, image.get());
322
323 // Switch R <-> B channels because WebPAnimEncoderAssemble()
324 // expects MODE_BGRA pictures.
325 {
326 LockImageBits<RgbTraits> bits(image.get(), Image::ReadWriteLock);
327 auto it = bits.begin(), end = bits.end();
328 for (; it != end; ++it) {
329 auto c = *it;
330 *it = rgba(rgba_getb(c), // Use blue in red channel
331 rgba_getg(c),
332 rgba_getr(c), // Use red in blue channel
333 rgba_geta(c));
334 }
335 }
336
337 if (!WebPAnimEncoderAdd(enc, &pic, timestamp_ms, &config)) {
338 if (!fop->isStop()) {
339 fop->setError("Error saving frame %d info\n", f);
340 return false;
341 }
342 else
343 return true;
344 }
345 timestamp_ms += sprite->frameDuration(f);
346
347 wd.f = f;
348 }
349 WebPAnimEncoderAdd(enc, nullptr, timestamp_ms, nullptr);
350
351 WebPData webp_data;
352 WebPDataInit(&webp_data);
353 WebPAnimEncoderAssemble(enc, &webp_data);
354 WebPAnimEncoderDelete(enc);
355
356 if (fwrite(webp_data.bytes, 1, webp_data.size, fp) != webp_data.size) {
357 fop->setError("Error saving content into file\n");
358 return false;
359 }
360
361 WebPDataClear(&webp_data);
362 return true;
363}
364
365#endif // ENABLE_SAVE
366
367// Shows the WebP configuration dialog.
368FormatOptionsPtr WebPFormat::onAskUserForFormatOptions(FileOp* fop)
369{
370 auto opts = fop->formatOptionsOfDocument<WebPOptions>();
371#ifdef ENABLE_UI
372 if (fop->context() && fop->context()->isUIAvailable()) {
373 try {
374 auto& pref = Preferences::instance();
375
376 if (pref.isSet(pref.webp.loop))
377 opts->setLoop(pref.webp.loop());
378
379 if (pref.isSet(pref.webp.type))
380 opts->setType(WebPOptions::Type(pref.webp.type()));
381
382 switch (opts->type()) {
383 case WebPOptions::Lossless:
384 if (pref.isSet(pref.webp.compression)) opts->setCompression(pref.webp.compression());
385 if (pref.isSet(pref.webp.imageHint)) opts->setImageHint(WebPImageHint(pref.webp.imageHint()));
386 break;
387 case WebPOptions::Lossy:
388 if (pref.isSet(pref.webp.quality)) opts->setQuality(pref.webp.quality());
389 if (pref.isSet(pref.webp.imagePreset)) opts->setImagePreset(WebPPreset(pref.webp.imagePreset()));
390 break;
391 }
392
393 if (pref.webp.showAlert()) {
394 app::gen::WebpOptions win;
395
396 auto updatePanels = [&win, &opts]{
397 int o = base::convert_to<int>(win.type()->getValue());
398 opts->setType(WebPOptions::Type(o));
399 win.losslessOptions()->setVisible(o == int(WebPOptions::Lossless));
400 win.lossyOptions()->setVisible(o == int(WebPOptions::Lossy));
401
402 auto rc = win.bounds();
403 win.setBounds(
404 gfx::Rect(rc.origin(),
405 win.sizeHint()));
406
407 auto manager = win.manager();
408 if (manager)
409 manager->invalidateRect(rc); // TODO this should be automatic
410 // when a window bounds is modified
411 };
412
413 win.loop()->setSelected(opts->loop());
414 win.type()->setSelectedItemIndex(int(opts->type()));
415 win.compression()->setValue(opts->compression());
416 win.imageHint()->setSelectedItemIndex(opts->imageHint());
417 win.quality()->setValue(static_cast<int>(opts->quality()));
418 win.imagePreset()->setSelectedItemIndex(opts->imagePreset());
419
420 updatePanels();
421 win.type()->Change.connect(updatePanels);
422
423 win.openWindowInForeground();
424
425 if (win.closer() == win.ok()) {
426 pref.webp.loop(win.loop()->isSelected());
427 pref.webp.type(base::convert_to<int>(win.type()->getValue()));
428 pref.webp.compression(win.compression()->getValue());
429 pref.webp.imageHint(base::convert_to<int>(win.imageHint()->getValue()));
430 pref.webp.quality(win.quality()->getValue());
431 pref.webp.imagePreset(base::convert_to<int>(win.imagePreset()->getValue()));
432
433 opts->setLoop(pref.webp.loop());
434 opts->setType(WebPOptions::Type(pref.webp.type()));
435 switch (opts->type()) {
436 case WebPOptions::Lossless:
437 opts->setCompression(pref.webp.compression());
438 opts->setImageHint(WebPImageHint(pref.webp.imageHint()));
439 break;
440 case WebPOptions::Lossy:
441 opts->setQuality(pref.webp.quality());
442 opts->setImagePreset(WebPPreset(pref.webp.imagePreset()));
443 break;
444 }
445 }
446 else {
447 opts.reset();
448 }
449 }
450 }
451 catch (const std::exception& e) {
452 Console::showException(e);
453 return std::shared_ptr<WebPOptions>(nullptr);
454 }
455 }
456#endif // ENABLE_UI
457 return opts;
458}
459
460} // namespace app
461