From aadf6b32b9cf2e4d1595cd5a08d88908e07c39e5 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Fri, 19 Aug 2022 01:45:22 +0200 Subject: [PATCH] Add option to copy or save subtitles with transparent background --- src/async_video_provider.cpp | 55 ++++++++++++++++++++++++++++++ src/async_video_provider.h | 12 +++++++ src/command/video.cpp | 38 ++++++++++++++++++--- src/libresrc/default_menu.json | 3 ++ src/libresrc/osx/default_menu.json | 3 ++ src/video_frame.cpp | 14 ++++++++ src/video_frame.h | 1 + 7 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/async_video_provider.cpp b/src/async_video_provider.cpp index 33dbe4bca..3801bddae 100644 --- a/src/async_video_provider.cpp +++ b/src/async_video_provider.cpp @@ -25,6 +25,12 @@ #include +#if BOOST_VERSION >= 106900 +#include +#else +#include +#endif + enum { NEW_SUBS_FILE = -1, SUBS_FILE_ALREADY_LOADED = -2 @@ -81,6 +87,55 @@ std::shared_ptr AsyncVideoProvider::ProcFrame(int frame_number, doub return frame; } +VideoFrame AsyncVideoProvider::GetBlankFrame(bool white) { + VideoFrame result; + result.width = GetWidth(); + result.height = GetHeight(); + result.pitch = result.width * 4; + result.flipped = false; + result.data.resize(result.pitch * result.height, white ? 255 : 0); + return result; +} + +VideoFrame AsyncVideoProvider::GetSubtitles(double time) { + // We want to combine all transparent subtitle layers onto one layer. + // Instead of alpha blending them all together, which can be messy and cause + // rounding errors, we draw them once on a black frame and once on a white frame, + // and solve for the color and alpha. This has the benefit of being independent + // of the subtitle provider, as long as the provider works by alpha blending. + VideoFrame frame_black = GetBlankFrame(false); + if (!subs) return frame_black; + VideoFrame frame_white = GetBlankFrame(true); + + subs_provider->LoadSubtitles(subs.get()); + subs_provider->DrawSubtitles(frame_black, time / 1000.); + subs_provider->DrawSubtitles(frame_white, time / 1000.); + + using namespace boost::gil; + auto blackview = interleaved_view(frame_black.width, frame_black.height, (bgra8_pixel_t*) frame_black.data.data(), frame_black.width * 4); + auto whiteview = interleaved_view(frame_white.width, frame_white.height, (bgra8_pixel_t*) frame_white.data.data(), frame_white.width * 4); + + transform_pixels(blackview, whiteview, blackview, [=](const bgra8_pixel_t black, const bgra8_pixel_t white) -> bgra8_pixel_t { + int a = 255 - (white[0] - black[0]); + + bgra8_pixel_t ret; + if (a == 0) { + ret[0] = 0; + ret[1] = 0; + ret[2] = 0; + ret[3] = 0; + } else { + ret[0] = black[0] / (a / 255.); + ret[1] = black[1] / (a / 255.); + ret[2] = black[2] / (a / 255.); + ret[3] = a; + } + return ret; + }); + + return frame_black; +} + static std::unique_ptr get_subs_provider(wxEvtHandler *evt_handler, agi::BackgroundRunner *br) { try { return SubtitlesProviderFactory::GetProvider(br); diff --git a/src/async_video_provider.h b/src/async_video_provider.h index ce5c83dbc..a3d9adbb5 100644 --- a/src/async_video_provider.h +++ b/src/async_video_provider.h @@ -78,6 +78,9 @@ class AsyncVideoProvider { std::vector> buffers; + // Returns a monochromatic frame with the current dimensions + VideoFrame GetBlankFrame(bool white); + public: /// @brief Load the passed subtitle file /// @param subs File to load @@ -108,6 +111,15 @@ public: /// @brief raw Get raw frame without subtitles std::shared_ptr GetFrame(int frame, double time, bool raw = false); + /// @brief Synchronously get the subtitles with transparent background + /// @brief time Exact start time of the frame in seconds + /// + /// This function is not used for drawing the subtitles on the screen and is not + /// guaranteed that drawing the resulting image on the current raw frame exactly + /// results in the current rendered frame with subtitles. This function is for + /// purposes like copying the current subtitles to the clipboard. + VideoFrame GetSubtitles(double time); + /// Ask the video provider to change YCbCr matricies void SetColorSpace(std::string const& matrix); diff --git a/src/command/video.cpp b/src/command/video.cpp index d3ffaf7fe..2fbdb8715 100644 --- a/src/command/video.cpp +++ b/src/command/video.cpp @@ -287,9 +287,13 @@ struct video_focus_seek final : public validator_video_loaded { } }; -wxImage get_image(agi::Context *c, bool raw) { +wxImage get_image(agi::Context *c, bool raw, bool subsonly = false) { auto frame = c->videoController->GetFrameN(); - return GetImage(*c->project->VideoProvider()->GetFrame(frame, c->project->Timecodes().TimeAtFrame(frame), raw)); + if (subsonly) { + return GetImageWithAlpha(c->project->VideoProvider()->GetSubtitles(c->project->Timecodes().TimeAtFrame(frame))); + } else { + return GetImage(*c->project->VideoProvider()->GetFrame(frame, c->project->Timecodes().TimeAtFrame(frame), raw)); + } } struct video_frame_copy final : public validator_video_loaded { @@ -314,6 +318,19 @@ struct video_frame_copy_raw final : public validator_video_loaded { } }; +struct video_frame_copy_subs final : public validator_video_loaded { + CMD_NAME("video/frame/copy/subs") + STR_MENU("Copy image to Clipboard (only subtitles)") + STR_DISP("Copy image to Clipboard (only subtitles)") + STR_HELP("Copy the currently displayed subtitles to the clipboard, with transparent background") + + void operator()(agi::Context *c) override { + wxBitmap img(get_image(c, false, true)); + img.UseAlpha(); + SetClipboard(img); + } +}; + struct video_frame_next final : public validator_video_loaded { CMD_NAME("video/frame/next") STR_MENU("Next Frame") @@ -456,7 +473,7 @@ struct video_frame_prev_large final : public validator_video_loaded { } }; -static void save_snapshot(agi::Context *c, bool raw) { +static void save_snapshot(agi::Context *c, bool raw, bool subsonly = false) { auto option = OPT_GET("Path/Screenshot")->GetString(); agi::fs::path basepath; @@ -491,7 +508,7 @@ static void save_snapshot(agi::Context *c, bool raw) { path = agi::format("%s_%03d_%d.png", basepath.string(), session_shot_count++, c->videoController->GetFrameN()); } while (agi::fs::FileExists(path)); - get_image(c, raw).SaveFile(to_wx(path), wxBITMAP_TYPE_PNG); + get_image(c, raw, subsonly).SaveFile(to_wx(path), wxBITMAP_TYPE_PNG); } struct video_frame_save final : public validator_video_loaded { @@ -516,6 +533,17 @@ struct video_frame_save_raw final : public validator_video_loaded { } }; +struct video_frame_save_subs final : public validator_video_loaded { + CMD_NAME("video/frame/save/subs") + STR_MENU("Save PNG snapshot (only subtitles)") + STR_DISP("Save PNG snapshot (only subtitles)") + STR_HELP("Save the currently displayed subtitles with transparent background to a PNG file in the video's directory") + + void operator()(agi::Context *c) override { + save_snapshot(c, false, true); + } +}; + struct video_jump final : public validator_video_loaded { CMD_NAME("video/jump") CMD_ICON(jumpto_button) @@ -751,6 +779,7 @@ namespace cmd { reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); + reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); @@ -761,6 +790,7 @@ namespace cmd { reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); + reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); diff --git a/src/libresrc/default_menu.json b/src/libresrc/default_menu.json index b1797836f..36f9c5d22 100644 --- a/src/libresrc/default_menu.json +++ b/src/libresrc/default_menu.json @@ -217,6 +217,9 @@ { "command" : "video/frame/save/raw" }, { "command" : "video/frame/copy/raw" }, {}, + { "command" : "video/frame/save/subs" }, + { "command" : "video/frame/copy/subs" }, + {}, { "command" : "video/copy_coordinates" } ] } diff --git a/src/libresrc/osx/default_menu.json b/src/libresrc/osx/default_menu.json index 75a9e4a03..a05e0f411 100644 --- a/src/libresrc/osx/default_menu.json +++ b/src/libresrc/osx/default_menu.json @@ -227,6 +227,9 @@ { "command" : "video/frame/save/raw" }, { "command" : "video/frame/copy/raw" }, {}, + { "command" : "video/frame/save/subs" }, + { "command" : "video/frame/copy/subs" }, + {}, { "command" : "video/copy_coordinates" } ] } diff --git a/src/video_frame.cpp b/src/video_frame.cpp index c7957d964..f105bfd53 100644 --- a/src/video_frame.cpp +++ b/src/video_frame.cpp @@ -48,3 +48,17 @@ wxImage GetImage(VideoFrame const& frame) { copy_and_convert_pixels(src, dst, color_converter()); return img; } + +wxImage GetImageWithAlpha(VideoFrame const &frame) { + wxImage img = GetImage(frame); + img.InitAlpha(); + uint8_t *dst = img.GetAlpha(); + const uint8_t *src = frame.data.data() + 3; + for (int y = 0; y < frame.height; y++) { + for (int x = 0; x < frame.width; x++) { + *(dst++) = *src; + src += 4; + } + } + return img; +} diff --git a/src/video_frame.h b/src/video_frame.h index 2a47ed69c..de88d03dd 100644 --- a/src/video_frame.h +++ b/src/video_frame.h @@ -27,3 +27,4 @@ struct VideoFrame { }; wxImage GetImage(VideoFrame const& frame); +wxImage GetImageWithAlpha(VideoFrame const& frame);