| 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 | |