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 | |
38 | namespace app { |
39 | |
40 | using namespace base; |
41 | |
42 | class 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 | |
74 | FileFormat* CreateWebPFormat() |
75 | { |
76 | return new WebPFormat; |
77 | } |
78 | |
79 | const 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 | |
94 | bool 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 | |
228 | struct 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 | |
238 | static 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 | |
254 | bool 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. |
368 | FormatOptionsPtr 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 | |