| /* |
| * Copyright 2018 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "SkCanvas.h" |
| #include "SkCommandLineFlags.h" |
| #include "SkGraphics.h" |
| #include "SkMakeUnique.h" |
| #include "SkOSFile.h" |
| #include "SkOSPath.h" |
| #include "Skottie.h" |
| #include "SkottieUtils.h" |
| #include "SkPictureRecorder.h" |
| #include "SkStream.h" |
| #include "SkSurface.h" |
| |
| #include <vector> |
| |
| DEFINE_string2(input , i, nullptr, "Input .json file."); |
| DEFINE_string2(writePath, w, nullptr, "Output directory. Frames are names [0-9]{6}.png."); |
| DEFINE_string2(format , f, "png" , "Output format (png or skp)"); |
| |
| DEFINE_double(t0, 0, "Timeline start [0..1]."); |
| DEFINE_double(t1, 1, "Timeline stop [0..1]."); |
| DEFINE_double(fps, 30, "Decode frames per second."); |
| |
| DEFINE_int32(width , 800, "Render width."); |
| DEFINE_int32(height, 600, "Render height."); |
| |
| namespace { |
| |
| class Sink { |
| public: |
| virtual ~Sink() = default; |
| Sink(const Sink&) = delete; |
| Sink& operator=(const Sink&) = delete; |
| |
| bool handleFrame(const sk_sp<skottie::Animation>& anim, size_t idx) const { |
| const auto frame_file = SkStringPrintf("0%06d.%s", idx, fExtension.c_str()); |
| SkFILEWStream stream (SkOSPath::Join(FLAGS_writePath[0], frame_file.c_str()).c_str()); |
| |
| if (!stream.isValid()) { |
| SkDebugf("Could not open '%s/%s' for writing.\n", |
| FLAGS_writePath[0], frame_file.c_str()); |
| return false; |
| } |
| |
| return this->saveFrame(anim, &stream); |
| } |
| |
| protected: |
| Sink(const char* ext) : fExtension(ext) {} |
| |
| virtual bool saveFrame(const sk_sp<skottie::Animation>& anim, SkFILEWStream*) const = 0; |
| |
| private: |
| const SkString fExtension; |
| }; |
| |
| class PNGSink final : public Sink { |
| public: |
| PNGSink() |
| : INHERITED("png") |
| , fSurface(SkSurface::MakeRasterN32Premul(FLAGS_width, FLAGS_height)) { |
| if (!fSurface) { |
| SkDebugf("Could not allocate a %d x %d surface.\n", FLAGS_width, FLAGS_height); |
| } |
| } |
| |
| bool saveFrame(const sk_sp<skottie::Animation>& anim, SkFILEWStream* stream) const override { |
| if (!fSurface) return false; |
| |
| auto* canvas = fSurface->getCanvas(); |
| SkAutoCanvasRestore acr(canvas, true); |
| |
| canvas->concat(SkMatrix::MakeRectToRect(SkRect::MakeSize(anim->size()), |
| SkRect::MakeIWH(FLAGS_width, FLAGS_height), |
| SkMatrix::kCenter_ScaleToFit)); |
| |
| canvas->clear(SK_ColorTRANSPARENT); |
| anim->render(canvas); |
| |
| auto png_data = fSurface->makeImageSnapshot()->encodeToData(); |
| if (!png_data) { |
| SkDebugf("Failed to encode frame!\n"); |
| return false; |
| } |
| |
| return stream->write(png_data->data(), png_data->size()); |
| } |
| |
| private: |
| const sk_sp<SkSurface> fSurface; |
| |
| using INHERITED = Sink; |
| }; |
| |
| class SKPSink final : public Sink { |
| public: |
| SKPSink() : INHERITED("skp") {} |
| |
| bool saveFrame(const sk_sp<skottie::Animation>& anim, SkFILEWStream* stream) const override { |
| SkPictureRecorder recorder; |
| |
| auto canvas = recorder.beginRecording(FLAGS_width, FLAGS_height); |
| canvas->concat(SkMatrix::MakeRectToRect(SkRect::MakeSize(anim->size()), |
| SkRect::MakeIWH(FLAGS_width, FLAGS_height), |
| SkMatrix::kCenter_ScaleToFit)); |
| anim->render(canvas); |
| recorder.finishRecordingAsPicture()->serialize(stream); |
| |
| return true; |
| } |
| |
| private: |
| const sk_sp<SkSurface> fSurface; |
| |
| using INHERITED = Sink; |
| }; |
| |
| class Logger final : public skottie::Logger { |
| public: |
| struct LogEntry { |
| SkString fMessage, |
| fJSON; |
| }; |
| |
| void log(skottie::Logger::Level lvl, const char message[], const char json[]) override { |
| auto& log = lvl == skottie::Logger::Level::kError ? fErrors : fWarnings; |
| log.push_back({ SkString(message), json ? SkString(json) : SkString() }); |
| } |
| |
| void report() const { |
| SkDebugf("Animation loaded with %lu error%s, %lu warning%s.\n", |
| fErrors.size(), fErrors.size() == 1 ? "" : "s", |
| fWarnings.size(), fWarnings.size() == 1 ? "" : "s"); |
| |
| const auto& show = [](const LogEntry& log, const char prefix[]) { |
| SkDebugf("%s%s", prefix, log.fMessage.c_str()); |
| if (!log.fJSON.isEmpty()) |
| SkDebugf(" : %s", log.fJSON.c_str()); |
| SkDebugf("\n"); |
| }; |
| |
| for (const auto& err : fErrors) show(err, " !! "); |
| for (const auto& wrn : fWarnings) show(wrn, " ?? "); |
| } |
| |
| private: |
| std::vector<LogEntry> fErrors, |
| fWarnings; |
| }; |
| |
| } // namespace |
| |
| int main(int argc, char** argv) { |
| SkCommandLineFlags::Parse(argc, argv); |
| SkAutoGraphics ag; |
| |
| if (FLAGS_input.isEmpty() || FLAGS_writePath.isEmpty()) { |
| SkDebugf("Missing required 'input' and 'writePath' args.\n"); |
| return 1; |
| } |
| |
| if (FLAGS_fps <= 0) { |
| SkDebugf("Invalid fps: %f.\n", FLAGS_fps); |
| return 1; |
| } |
| |
| if (!sk_mkdir(FLAGS_writePath[0])) { |
| return 1; |
| } |
| |
| std::unique_ptr<Sink> sink; |
| if (0 == strcmp(FLAGS_format[0], "png")) { |
| sink = skstd::make_unique<PNGSink>(); |
| } else if (0 == strcmp(FLAGS_format[0], "skp")) { |
| sink = skstd::make_unique<SKPSink>(); |
| } else { |
| SkDebugf("Unknown format: %s\n", FLAGS_format[0]); |
| return 1; |
| } |
| |
| auto logger = sk_make_sp<Logger>(); |
| |
| auto anim = skottie::Animation::Builder() |
| .setLogger(logger) |
| .setResourceProvider( |
| skottie_utils::FileResourceProvider::Make(SkOSPath::Dirname(FLAGS_input[0]))) |
| .makeFromFile(FLAGS_input[0]); |
| if (!anim) { |
| SkDebugf("Could not load animation: '%s'.\n", FLAGS_input[0]); |
| return 1; |
| } |
| |
| logger->report(); |
| |
| static constexpr double kMaxFrames = 10000; |
| const auto t0 = SkTPin(FLAGS_t0, 0.0, 1.0), |
| t1 = SkTPin(FLAGS_t1, t0, 1.0), |
| advance = 1 / std::min(anim->duration() * FLAGS_fps, kMaxFrames); |
| |
| size_t frame_index = 0; |
| for (auto t = t0; t <= t1; t += advance) { |
| anim->seek(t); |
| sink->handleFrame(anim, frame_index++); |
| } |
| |
| return 0; |
| } |