From a5aa4eca2930a0813aef6c1373eeb2a53eaa2959 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Mon, 31 Oct 2022 23:03:37 +0100 Subject: [PATCH 01/13] vapoursynth: Properly pass through script errors --- src/audio_provider_vs.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/audio_provider_vs.cpp b/src/audio_provider_vs.cpp index 575daaad6..37005d3c1 100644 --- a/src/audio_provider_vs.cpp +++ b/src/audio_provider_vs.cpp @@ -84,7 +84,9 @@ VapoursynthAudioProvider::VapoursynthAudioProvider(agi::fs::path const& filename num_samples = vi->numSamples; } catch (VapoursynthError const& err) { - throw agi::AudioProviderError(agi::format("Vapoursynth error: %s", err.GetMessage())); + // Unlike the video provider manager, the audio provider factory catches AudioProviderErrors and picks whichever source doesn't throw one. + // So just rethrow the Error here with an extra label so the user will see the error message and know the audio wasn't loaded with VS + throw VapoursynthError(agi::format("Vapoursynth error: %s", err.GetMessage())); } template From 962295cf1e4f7327cb7b843abdcee5c3a8fadabe Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Tue, 1 Nov 2022 00:15:57 +0100 Subject: [PATCH 02/13] vapoursynth: Fix offset in audio block's first frame --- src/audio_provider_vs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/audio_provider_vs.cpp b/src/audio_provider_vs.cpp index 37005d3c1..b41b46773 100644 --- a/src/audio_provider_vs.cpp +++ b/src/audio_provider_vs.cpp @@ -117,7 +117,7 @@ void VapoursynthAudioProvider::FillBufferWithFrame(void *buf, int n, int64_t sta std::vector planes(channels); for (int c = 0; c < channels; c++) { - planes[c] = vs.GetAPI()->getReadPtr(frame, c); + planes[c] = vs.GetAPI()->getReadPtr(frame, c) + bytes_per_sample * start; if (planes[c] == nullptr) { vs.GetAPI()->freeFrame(frame); throw VapoursynthError("Failed to read audio channel"); From 3583e57ddce4fc5a19a705e30657e39a2303c4aa Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Tue, 1 Nov 2022 11:26:49 +0100 Subject: [PATCH 03/13] vapoursynth: use exact fraction for frame rate --- src/video_provider_vs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_provider_vs.cpp b/src/video_provider_vs.cpp index a0a006f43..724df2cb2 100644 --- a/src/video_provider_vs.cpp +++ b/src/video_provider_vs.cpp @@ -137,7 +137,7 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename // Assume constant frame rate, since handling VFR would require going through all frames when loading. // Users can load custom timecodes files to deal with VFR. // Alternatively (TODO) the provider could read timecodes and keyframes from a second output node. - fps = (double) vi->fpsNum / vi->fpsDen; + fps = agi::vfr::Framerate(vi->fpsNum, vi->fpsDen); // Find the first frame to get some info const VSFrame *frame; From 245cc68afabefbc9290bd5a13ec327a59fe23b6d Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Tue, 1 Nov 2022 11:26:57 +0100 Subject: [PATCH 04/13] Fix FrameAtTime computation for CFR The new formula is just the inverse function of the CFR part of the TimeAtFrame function. To see how the previous implementation was faulty, see either the added tests, or - In Aegisub, open a dummy video with a frame rate of 23.976 - Make a subtitle event with start time 04:44.41 - Double-click the line to (supposedly) seek to its first frame - This will seek one frame earlier than it should, and the event will not be displayed on the resulting frame. --- libaegisub/common/vfr.cpp | 2 +- tests/tests/vfr.cpp | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/libaegisub/common/vfr.cpp b/libaegisub/common/vfr.cpp index c82fcdbee..377e29b34 100644 --- a/libaegisub/common/vfr.cpp +++ b/libaegisub/common/vfr.cpp @@ -225,7 +225,7 @@ int Framerate::FrameAtTime(int ms, Time type) const { return int((ms * numerator / denominator - 999) / 1000); if (ms > timecodes.back()) - return int((ms * numerator - last + denominator - 1) / denominator / 1000) + (int)timecodes.size() - 1; + return int((ms * numerator - numerator / 2 - last + numerator - 1) / denominator / 1000) + (int)timecodes.size() - 1; return (int)distance(lower_bound(timecodes.rbegin(), timecodes.rend(), ms, std::greater()), timecodes.rend()) - 1; } diff --git a/tests/tests/vfr.cpp b/tests/tests/vfr.cpp index a9b8f3acf..90a02d4c6 100644 --- a/tests/tests/vfr.cpp +++ b/tests/tests/vfr.cpp @@ -149,6 +149,12 @@ TEST(lagi_vfr, cfr_round_trip_exact) { for (int i = -10; i < 11; i++) { EXPECT_EQ(i, fps.FrameAtTime(fps.TimeAtFrame(i))); } + + ASSERT_NO_THROW(fps = Framerate(24000, 1001)); + int frames[] = {-100, -10, -1, 0, 1, 10, 100, 6820}; + for (int i : frames) { + EXPECT_EQ(i, fps.FrameAtTime(fps.TimeAtFrame(i))); + } } TEST(lagi_vfr, cfr_round_trip_start) { @@ -157,6 +163,12 @@ TEST(lagi_vfr, cfr_round_trip_start) { for (int i = -10; i < 11; i++) { EXPECT_EQ(i, fps.FrameAtTime(fps.TimeAtFrame(i, START), START)); } + + ASSERT_NO_THROW(fps = Framerate(24000, 1001)); + int frames[] = {-100, -10, -1, 0, 1, 10, 100, 6820}; + for (int i : frames) { + EXPECT_EQ(i, fps.FrameAtTime(fps.TimeAtFrame(i, START), START)); + } } TEST(lagi_vfr, cfr_round_trip_end) { @@ -165,6 +177,12 @@ TEST(lagi_vfr, cfr_round_trip_end) { for (int i = -10; i < 11; i++) { EXPECT_EQ(i, fps.FrameAtTime(fps.TimeAtFrame(i, END), END)); } + + ASSERT_NO_THROW(fps = Framerate(24000, 1001)); + int frames[] = {-100, -10, -1, 0, 1, 10, 100, 6820}; + for (int i : frames) { + EXPECT_EQ(i, fps.FrameAtTime(fps.TimeAtFrame(i, END), END)); + } } TEST(lagi_vfr, vfr_round_trip_exact) { From a0d381019f29df2e53e8c15e260c81cded31c9c1 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 2 Nov 2022 19:50:25 +0100 Subject: [PATCH 05/13] README: wangqr_gui and some general info --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 405cada7f..5735ca337 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ We absolutely do, and I'm aware that adding another one [doesn't sound like](htt - [AegisubDC](https://github.com/Ristellise/AegisubDC) has the most modern features (in particular video-panning), but is Windows-only and not actively maintained anymore. - [The TypesettingTools fork](https://github.com/TypesettingTools/Aegisub) is the one that will one day become the upstream version and builds relatively effortlessly on all operating systems, but at the moment it's not moving much. It's the base for this fork, and I hope to one day merge most of these additions into it. - Only PR'ing the changes in here to various forks would cause even more chaos -- I try to convince myself that this isn't really a "fork" in the traditional sense - one which aims to provide extended support and stability fixes. It's a collection of new feature additions which I built myself, together with some of the most important new features floating around other forks. +- ~~I try to convince myself that this isn't really a "fork" in the traditional sense - one which aims to provide extended support and stability fixes. It's a collection of new feature additions which I built myself, together with some of the most important new features floating around other forks.~~ At this point it's probably too late to still be saying this. Still, the general mission hasn't changed. This fork collects new features and critical bugfixes, but won't be putting extra time into maintenance aspects like cleanup and refactors. Partly, this is also because any big refactors would make it harder to pull these changes into upstream repositories or future forks. - While this is usually also the version of Aegisub I'm currently using, I make absolutely no promises on stability. **Don't** use this version if you're just looking for any version of Aegisub - this is mostly intended for typesetting and other advanced usage. + While this is usually also the version of Aegisub I'm currently using, I make no promises on stability. **Don't** use this version if you're just looking for any version of Aegisub - this is mostly intended for typesetting and other advanced usage. ### Organization Being a collection of different feature additions, this repository consists of a set of branches for different features, so that they can easily be merged into other repositories. The [`feature`](https://github.com/arch1t3cht/Aegisub/tree/feature) branch merges together all the features I deem as currently usable. Due to the structure of the repository, I will be force-pushing to this branch and some of the individual branches very frequently, so they're not ideal for basing further branches on. @@ -31,10 +31,11 @@ This list is for navigating the repository. Go to the [release page](https://git - [`workarounds`](https://github.com/arch1t3cht/Aegisub/tree/workarounds): Same as `bugfixes`, but these are hacky fixes that probably shouldn't be pulled without more work. - [`fixes`](https://github.com/arch1t3cht/Aegisub/tree/fixes): Miscellaneous bugfixes - [`misc`](https://github.com/arch1t3cht/Aegisub/tree/misc): Other miscellaneous additions +- [`wangqr_gui`](https://github.com/arch1t3cht/Aegisub/tree/wangqr_gui): Merge wangqr's changes regarding the GUI. In particular, add high-DPI compatibility. - [`misc_dc`](https://github.com/arch1t3cht/Aegisub/tree/misc_dc): Miscellaneous changes taken from AegisubDC - [`xa2-ds`](https://github.com/arch1t3cht/Aegisub/tree/xa2-ds): Add XAudio2 backend and allow stereo playback for some other backends, by wangqr and Shinon. - [`stereo`](https://github.com/arch1t3cht/Aegisub/tree/stereo): Add multi-channel support for the other audio backends where possible. -- [`video_panning_feature`](https://github.com/arch1t3cht/Aegisub/tree/video_panning_feature): Merge [moex3's video zoom and panning](https://github.com/TypesettingTools/Aegisub/pull/150), with an OSX fix and more options to control zoom behavior +- [`video_panning_feature`](https://github.com/arch1t3cht/Aegisub/tree/video_panning_feature): Merge [moex3's video zoom and panning](https://github.com/TypesettingTools/Aegisub/pull/150), with several bugfixes and more options to control zoom behavior - [`spectrum-frequency-mapping`](https://github.com/arch1t3cht/Aegisub/tree/spectrum-frequency-mapping): Merge EleonoreMizo's [spectrum display improvements](https://github.com/TypesettingTools/Aegisub/pull/94), and also make Shift+Scroll vertically zoom the audio display - [`wangqr_time_video`](https://github.com/arch1t3cht/Aegisub/tree/wangqr_time_video): Merge wangqr's feature adding a tool for timing subtitles to changes in the video @@ -64,11 +65,13 @@ If you're compiling yourself, try adding `--force-fallback-for=zlib` to the meso ### Compilation For compilation on Windows, see the TSTools documentation below. Also check the [GitHub workflow](https://github.com/arch1t3cht/Aegisub/blob/cibuilds/.github/workflows/ci.yml) for the project arguments. -On Linux, you can use the [TSTools PKGBUILD](https://aur.archlinux.org/packages/aegisub-ttools-meson-git) as a base, in particular for installing the necessary dependencies if you don't want to compile them yourself. -To compile manually, -- Install Meson (at the moment, you'll need to downgrade Meson below 0.63.0: `pip install meson==0.62.2`) +On Arch Linux, there is an AUR package called [aegisub-arch1t3cht-git](https://aur.archlinux.org/packages/aegisub-arch1t3cht-git). It's not maintained by me but seems to work. + +On other distributions or for manual compilation you can use this package or the [TSTools PKGBUILD](https://aur.archlinux.org/packages/aegisub-ttools-meson-git) as a reference, in particular for installing the necessary dependencies if you don't want to compile them yourself. +If all dependencies are installed: +- Install Meson - Clone the repository -- In the repository, run `meson setup build` for the default configuration. See below for further options. +- In the repository, run `meson setup build --buildtype=release` for the default configuration. See below for further options. - `cd` to the `build` directory and run `ninja` - You'll get an `aegisub` binary in the `build` folder. To install it to a system-wide location, run `ninja install`. To install to `/usr` instead of `/usr/local`, pass `--prefix=/usr` when configuring or reconfiguring meson. - When recompiling after pulling new commits, skip the `meson setup` setup and just immediately run `ninja` from the build directory - even when the build configuration changed. From 04ccc15fd48d320a1a2c5e61a7282e8f24f6b928 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 2 Nov 2022 21:49:30 +0100 Subject: [PATCH 06/13] Fix overscan mask with DPI scaling --- src/video_display.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/video_display.cpp b/src/video_display.cpp index 8d85311a6..1919fc577 100644 --- a/src/video_display.cpp +++ b/src/video_display.cpp @@ -227,19 +227,19 @@ catch (const agi::Exception &err) { } void VideoDisplay::DrawOverscanMask(float horizontal_percent, float vertical_percent) const { - Vector2D v(viewport_width, viewport_height); + Vector2D v = Vector2D(viewport_width, viewport_height) / scale_factor; Vector2D size = Vector2D(horizontal_percent, vertical_percent) / 2 * v; // Clockwise from top-left Vector2D corners[] = { size, - Vector2D(viewport_width - size.X(), size), + Vector2D(viewport_width / scale_factor - size.X(), size), v - size, - Vector2D(size, viewport_height - size.Y()) + Vector2D(size, viewport_height / scale_factor - size.Y()) }; // Shift to compensate for black bars - Vector2D pos(viewport_left, viewport_top); + Vector2D pos = Vector2D(viewport_left, viewport_top) / scale_factor; for (auto& corner : corners) corner = corner + pos; @@ -261,7 +261,7 @@ void VideoDisplay::DrawOverscanMask(float horizontal_percent, float vertical_per std::vector vstart(1, 0); std::vector vcount(1, count); - gl.DrawMultiPolygon(points, vstart, vcount, Vector2D(viewport_left, viewport_top), Vector2D(viewport_width, viewport_height), true); + gl.DrawMultiPolygon(points, vstart, vcount, pos, v, true); } void VideoDisplay::PositionVideo() { From 2ee6f6e9040f039ce69ed94096e5a9a236947cbc Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 2 Nov 2022 21:50:01 +0100 Subject: [PATCH 07/13] Fix overscan mask being half as thick as it should be The percent values used for the overscan masks follow the BBC's guidelines, as in https://en.wikipedia.org/wiki/Overscan#Overscan_amounts . However, these measure the per-side width as opposed to the total percentage of width/height being cut off. Thus, they should not be divided by two when drawing the mask. --- src/video_display.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_display.cpp b/src/video_display.cpp index 1919fc577..100f5f539 100644 --- a/src/video_display.cpp +++ b/src/video_display.cpp @@ -228,7 +228,7 @@ catch (const agi::Exception &err) { void VideoDisplay::DrawOverscanMask(float horizontal_percent, float vertical_percent) const { Vector2D v = Vector2D(viewport_width, viewport_height) / scale_factor; - Vector2D size = Vector2D(horizontal_percent, vertical_percent) / 2 * v; + Vector2D size = Vector2D(horizontal_percent, vertical_percent) * v; // Clockwise from top-left Vector2D corners[] = { From 28db5d31ce13477edaa9ed28d834a0f4337ef1fa Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 2 Nov 2022 23:07:55 +0100 Subject: [PATCH 08/13] video zoom: Fix crash on windows when making detached video too small --- src/video_display.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_display.cpp b/src/video_display.cpp index c941f7294..fc9ead8cd 100644 --- a/src/video_display.cpp +++ b/src/video_display.cpp @@ -201,7 +201,7 @@ void VideoDisplay::Render() try { E(glMatrixMode(GL_PROJECTION)); E(glLoadIdentity()); - E(glOrtho(0.0f, client_w, client_h, 0.0f, -1000.0f, 1000.0f)); + E(glOrtho(0.0f, std::max(client_w, 1), std::max(client_h, 1), 0.0f, -1000.0f, 1000.0f)); if (OPT_GET("Video/Overscan Mask")->GetBool()) { double ar = con->videoController->GetAspectRatioValue(); From 4edd61922934b1595251a3326e46fb3ee18b7071 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 2 Nov 2022 23:08:36 +0100 Subject: [PATCH 09/13] README: Typo fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5735ca337..8bcd14acc 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ This list is for navigating the repository. Go to the [release page](https://git - [`misc_dc`](https://github.com/arch1t3cht/Aegisub/tree/misc_dc): Miscellaneous changes taken from AegisubDC - [`xa2-ds`](https://github.com/arch1t3cht/Aegisub/tree/xa2-ds): Add XAudio2 backend and allow stereo playback for some other backends, by wangqr and Shinon. - [`stereo`](https://github.com/arch1t3cht/Aegisub/tree/stereo): Add multi-channel support for the other audio backends where possible. -- [`video_panning_feature`](https://github.com/arch1t3cht/Aegisub/tree/video_panning_feature): Merge [moex3's video zoom and panning](https://github.com/TypesettingTools/Aegisub/pull/150), with several bugfixes and more options to control zoom behavior +- [`video_panning_option`](https://github.com/arch1t3cht/Aegisub/tree/video_panning_option): Merge [moex3's video zoom and panning](https://github.com/TypesettingTools/Aegisub/pull/150), with several bugfixes and more options to control zoom behavior - [`spectrum-frequency-mapping`](https://github.com/arch1t3cht/Aegisub/tree/spectrum-frequency-mapping): Merge EleonoreMizo's [spectrum display improvements](https://github.com/TypesettingTools/Aegisub/pull/94), and also make Shift+Scroll vertically zoom the audio display - [`wangqr_time_video`](https://github.com/arch1t3cht/Aegisub/tree/wangqr_time_video): Merge wangqr's feature adding a tool for timing subtitles to changes in the video From 399042e9e9bf92341215b13f3924dbd28e934460 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 2 Nov 2022 23:48:59 +0100 Subject: [PATCH 10/13] Revert "fix" This was a test commit I never meant to push... This reverts commit cc9d13d21a50ec7be5a80a3fc09f6ae4f1b8a542. --- src/res/aegisub.exe.manifest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/res/aegisub.exe.manifest b/src/res/aegisub.exe.manifest index 5867c4a13..9ea55ca4b 100644 --- a/src/res/aegisub.exe.manifest +++ b/src/res/aegisub.exe.manifest @@ -1,6 +1,6 @@ - Aegisub subtitle editor + Aegisub Subtitle Editor From a3b131a3123035b53df08e3b1220bca3c13521e0 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Tue, 1 Nov 2022 20:16:56 +0100 Subject: [PATCH 11/13] vapoursynth: Reset locale after initializing On Windows, Python changes the application's locale upon being called, which will break wxwidgets, causing various assertion error dialogs or even crashes (for example when interacting with any sort of float edit control). Saving the locale and restoring it afterwards seems to be the only really possible way to remedy this. --- src/vapoursynth_common.cpp | 6 ++++-- src/vapoursynth_wrap.cpp | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/vapoursynth_common.cpp b/src/vapoursynth_common.cpp index 617849360..52614528c 100644 --- a/src/vapoursynth_common.cpp +++ b/src/vapoursynth_common.cpp @@ -23,15 +23,17 @@ #include int OpenScriptOrVideo(const VSSCRIPTAPI *api, VSScript *script, agi::fs::path const& filename, std::string default_script) { + int result; if (agi::fs::HasExtension(filename, "py") || agi::fs::HasExtension(filename, "vpy")) { - return api->evaluateFile(script, filename.string().c_str()); + result = api->evaluateFile(script, filename.string().c_str()); } else { std::string fname = filename.string(); boost::replace_all(fname, "\\", "\\\\"); boost::replace_all(fname, "'", "\\'"); std::string vscript = "filename = '" + fname + "'\n" + default_script; - return api->evaluateBuffer(script, vscript.c_str(), "aegisub"); + result = api->evaluateBuffer(script, vscript.c_str(), "aegisub"); } + return result; } #endif // WITH_VAPOURSYNTH diff --git a/src/vapoursynth_wrap.cpp b/src/vapoursynth_wrap.cpp index d0d0fbd8b..ac2b87dff 100644 --- a/src/vapoursynth_wrap.cpp +++ b/src/vapoursynth_wrap.cpp @@ -78,7 +78,12 @@ VapourSynthWrapper::VapourSynthWrapper() { if (!getVSScriptAPI) throw VapoursynthError("Failed to get address of getVSScriptAPI from " VSSCRIPT_SO); + // Python will set the program's locale to the user's default locale, which will break + // half of wxwidgets on some operating systems due to locale mismatches. There's not really anything + // we can do to fix it except for saving it and setting it back to its original value afterwards. + std::string oldlocale(setlocale(LC_ALL, NULL)); scriptapi = getVSScriptAPI(VSSCRIPT_API_VERSION); + setlocale(LC_ALL, oldlocale.c_str()); if (!scriptapi) throw VapoursynthError("Failed to get Vapoursynth ScriptAPI"); From b7f3e19e008ce62d234d7b54bb7452cf51275562 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Tue, 1 Nov 2022 20:25:05 +0100 Subject: [PATCH 12/13] vapoursynth: Don't update script colorspace if colorspace unknown Aegisub will automatically override the YCbCr Matrix field of the current file's Script Properties with the video's reported color space. The FFMS2 provider guesses a color space for all videos, but we don't do this for Vapoursynth. Thus, we now disable this overriding whenever the colorspace isn't known. --- src/video_provider_vs.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/video_provider_vs.cpp b/src/video_provider_vs.cpp index 724df2cb2..8e110eb3f 100644 --- a/src/video_provider_vs.cpp +++ b/src/video_provider_vs.cpp @@ -63,11 +63,12 @@ public: int GetHeight() const override { return vi->height; } double GetDAR() const override { return dar; } std::vector GetKeyFrames() const override { return keyframes; } - std::string GetColorSpace() const override { return colorspace; } - std::string GetRealColorSpace() const override { return colorspace; } + std::string GetColorSpace() const override { return GetRealColorSpace(); } + std::string GetRealColorSpace() const override { return colorspace == "Unknown" ? "None" : colorspace; } bool HasAudio() const override { return false; } - virtual bool WantsCaching() const override { return true; } - virtual std::string GetDecoderName() const override { return "VapourSynth"; } + bool WantsCaching() const override { return true; } + std::string GetDecoderName() const override { return "VapourSynth"; } + bool ShouldSetVideoProperties() const override { return colorspace != "Unknown"; } }; std::string colormatrix_description(int colorFamily, int colorRange, int matrix) { @@ -90,7 +91,7 @@ std::string colormatrix_description(int colorFamily, int colorRange, int matrix) case VSC_MATRIX_ST240_M: return str + ".240M"; default: - return "None"; + return "Unknown"; // Will return "None" in GetColorSpace } } From 7ddfef75174e0bb85ee6fb0b783ad065ffd1fcd8 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 2 Nov 2022 19:24:42 +0100 Subject: [PATCH 13/13] Add syntax highlighting for drawings and vector clips The highlighting distinguishes drawing commands from coordinates, and colors x and y coordinates in different colors to make coordinates easier to visually parse. Furthermore, in cubic Bezier curves, it underlines the coordinates which corresponds to endpoints of the curves. --- libaegisub/ass/dialogue_parser.cpp | 115 +++++++++++++++++- .../include/libaegisub/ass/dialogue_parser.h | 13 +- src/libresrc/default_config.json | 15 ++- src/libresrc/osx/default_config.json | 15 ++- src/preferences.cpp | 6 +- src/subs_edit_ctrl.cpp | 13 +- tests/tests/syntax_highlight.cpp | 37 +++++- tests/tests/word_split.cpp | 6 +- 8 files changed, 201 insertions(+), 19 deletions(-) diff --git a/libaegisub/ass/dialogue_parser.cpp b/libaegisub/ass/dialogue_parser.cpp index ea486dcf7..8cd1743e7 100644 --- a/libaegisub/ass/dialogue_parser.cpp +++ b/libaegisub/ass/dialogue_parser.cpp @@ -60,7 +60,11 @@ public: case dt::ERROR: SetStyling(tok.length, ss::ERROR); break; case dt::ARG: SetStyling(tok.length, ss::PARAMETER); break; case dt::COMMENT: SetStyling(tok.length, ss::COMMENT); break; - case dt::DRAWING: SetStyling(tok.length, ss::DRAWING); break; + case dt::DRAWING_CMD:SetStyling(tok.length, ss::DRAWING_CMD);break; + case dt::DRAWING_X: SetStyling(tok.length, ss::DRAWING_X); break; + case dt::DRAWING_Y: SetStyling(tok.length, ss::DRAWING_Y); break; + case dt::DRAWING_ENDPOINT_X: SetStyling(tok.length, ss::DRAWING_ENDPOINT_X); break; + case dt::DRAWING_ENDPOINT_Y: SetStyling(tok.length, ss::DRAWING_ENDPOINT_Y); break; case dt::TEXT: SetStyling(tok.length, ss::NORMAL); break; case dt::TAG_NAME: SetStyling(tok.length, ss::TAG); break; case dt::OPEN_PAREN: case dt::CLOSE_PAREN: case dt::ARG_SEP: case dt::TAG_START: @@ -72,6 +76,8 @@ public: case dt::WHITESPACE: if (ranges.size() && ranges.back().type == ss::PARAMETER) SetStyling(tok.length, ss::PARAMETER); + else if (ranges.size() && ranges.back().type == ss::DRAWING_ENDPOINT_X) + SetStyling(tok.length, ss::DRAWING_ENDPOINT_X); // connect the underline between x and y of endpoints else SetStyling(tok.length, ss::NORMAL); break; @@ -118,6 +124,64 @@ class WordSplitter { } } + void SplitDrawing(size_t &i) { + size_t starti = i; + + // First, split into words + size_t dpos = pos; + size_t tlen = 0; + bool tokentype = text[pos] == ' ' || text[pos] == '\t'; + while (tlen < tokens[i].length) { + bool newtype = text[dpos] == ' ' || text[dpos] == '\t'; + if (newtype != tokentype) { + tokentype = newtype; + SwitchTo(i, tokentype ? dt::DRAWING_FULL : dt::WHITESPACE, tlen); + tokens[i].type = tokentype ? dt::WHITESPACE : dt::DRAWING_FULL; + tlen = 0; + } + ++tlen; + ++dpos; + } + + // Then, label all the tokens + dpos = pos; + int num_coord = 0; + char lastcmd = ' '; + + for (size_t j = starti; j <= i; j++) { + char c = text[dpos]; + if (tokens[j].type == dt::WHITESPACE) { + } else if (c == 'm' || c == 'n' || c == 'l' || c == 's' || c == 'b' || c == 'p' || c == 'c') { + tokens[j].type = dt::DRAWING_CMD; + + if (tokens[j].length != 1) + tokens[j].type = dt::ERROR; + if (num_coord % 2 != 0) + tokens[j].type = dt::ERROR; + + lastcmd = c; + num_coord = 0; + } else { + bool valid = true; + for (size_t k = 0; k < tokens[j].length; k++) { + char c = text[dpos + k]; + if (!((c >= '0' && c <= '9') || c == '.' || c == '-' || c == 'e')) { + valid = false; + } + } + if (!valid) + tokens[j].type = dt::ERROR; + else if (lastcmd == 'b' && num_coord % 6 >= 4) + tokens[j].type = num_coord % 2 == 0 ? dt::DRAWING_ENDPOINT_X : dt::DRAWING_ENDPOINT_Y; + else + tokens[j].type = num_coord % 2 == 0 ? dt::DRAWING_X : dt::DRAWING_Y; + ++num_coord; + } + + dpos += tokens[j].length; + } + } + public: WordSplitter(std::string const& text, std::vector &tokens) : text(text) @@ -131,6 +195,9 @@ public: size_t len = tokens[i].length; if (tokens[i].type == dt::TEXT) SplitText(i); + else if (tokens[i].type == dt::DRAWING_FULL) { + SplitDrawing(i); + } pos += len; } } @@ -163,9 +230,51 @@ void MarkDrawings(std::string const& str, std::vector &tokens) { switch (tokens[i].type) { case dt::TEXT: if (in_drawing) - tokens[i].type = dt::DRAWING; + tokens[i].type = dt::DRAWING_FULL; break; case dt::TAG_NAME: + if (i + 3 < tokens.size() && (len == 4 || len == 5) && !strncmp(str.c_str() + pos + len - 4, "clip", 4)) { + if (tokens[i + 1].type != dt::OPEN_PAREN) + goto tag_p; + + size_t drawing_start = 0; + size_t drawing_end = 0; + + // Try to find a vector clip + for (size_t j = i + 2; j < tokens.size(); j++) { + if (tokens[j].type == dt::ARG_SEP) { + if (drawing_start) { + break; // More than two arguents - this is a rectangular clip + } + drawing_start = j + 1; + } else if (tokens[j].type == dt::CLOSE_PAREN) { + drawing_end = j; + break; + } else if (tokens[j].type != dt::WHITESPACE && tokens[j].type != dt::ARG) { + break; + } + } + + if (!drawing_end) + goto tag_p; + if (!drawing_start) + drawing_start = i + 2; + if (drawing_end == drawing_start + 1) + goto tag_p; + + // We found a clip between drawing_start and drawing_end. Now, join + // all the tokens into one and label it as a drawing. + size_t tokenlen = 0; + for (size_t j = drawing_start; j < drawing_end; j++) { + tokenlen += tokens[j].length; + } + + tokens[drawing_start].length = tokenlen; + tokens[drawing_start].type = dt::DRAWING_FULL; + tokens.erase(tokens.begin() + drawing_start + 1, tokens.begin() + drawing_end); + last_ovr_end -= drawing_end - drawing_start - 1; + } +tag_p: if (len != 1 || i + 1 >= tokens.size() || str[pos] != 'p') break; @@ -199,7 +308,7 @@ void MarkDrawings(std::string const& str, std::vector &tokens) { case dt::KARAOKE_VARIABLE: break; case dt::LINE_BREAK: break; default: - tokens[i].type = in_drawing ? dt::DRAWING : dt::TEXT; + tokens[i].type = in_drawing ? dt::DRAWING_FULL : dt::TEXT; if (i > 0 && tokens[i - 1].type == tokens[i].type) { tokens[i - 1].length += tokens[i].length; tokens.erase(tokens.begin() + i); diff --git a/libaegisub/include/libaegisub/ass/dialogue_parser.h b/libaegisub/include/libaegisub/ass/dialogue_parser.h index 0aa3f9962..727c0569f 100644 --- a/libaegisub/include/libaegisub/ass/dialogue_parser.h +++ b/libaegisub/include/libaegisub/ass/dialogue_parser.h @@ -39,7 +39,12 @@ namespace agi { ERROR, COMMENT, WHITESPACE, - DRAWING, + DRAWING_FULL, + DRAWING_CMD, + DRAWING_X, + DRAWING_Y, + DRAWING_ENDPOINT_X, + DRAWING_ENDPOINT_Y, KARAOKE_TEMPLATE, KARAOKE_VARIABLE }; @@ -49,7 +54,11 @@ namespace agi { enum { NORMAL = 0, COMMENT, - DRAWING, + DRAWING_CMD, + DRAWING_X, + DRAWING_Y, + DRAWING_ENDPOINT_X, + DRAWING_ENDPOINT_Y, OVERRIDE, PUNCTUATION, TAG, diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index 318f8d3ee..9154556bb 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -228,7 +228,9 @@ "Background" : { "Brackets" : "", "Comment" : "", - "Drawing" : "", + "Drawing Command" : "", + "Drawing X" : "", + "Drawing Y" : "", "Error" : "rgb(255, 200, 200)", "Karaoke Template" : "", "Karaoke Variable" : "", @@ -241,7 +243,9 @@ "Bold" : { "Brackets" : false, "Comment" : true, - "Drawing" : true, + "Drawing Command" : true, + "Drawing X" : false, + "Drawing Y" : false, "Error" : false, "Karaoke Template" : true, "Karaoke Variable" : true, @@ -251,9 +255,14 @@ "Slashes" : false, "Tags" : true }, + "Underline": { + "Drawing Endpoint": true + }, "Brackets" : "rgb(20, 50, 255)", "Comment" : "rgb(0,0,0)", - "Drawing" : "rgb(0,0,0)", + "Drawing Command" : "rgb(0,0,0)", + "Drawing X" : "rgb(90,40,40)", + "Drawing Y" : "rgb(40,90,40)", "Error" : "rgb(200, 0, 0)", "Karaoke Template" : "rgb(128, 0, 192)", "Karaoke Variable" : "rgb(128, 0, 192)", diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index 59f2ed05f..4405cd5cc 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -228,7 +228,9 @@ "Background" : { "Brackets" : "", "Comment" : "", - "Drawing" : "", + "Drawing Command" : "", + "Drawing X" : "", + "Drawing Y" : "", "Error" : "rgb(255, 200, 200)", "Karaoke Template" : "", "Karaoke Variable" : "", @@ -241,7 +243,9 @@ "Bold" : { "Brackets" : false, "Comment" : true, - "Drawing" : true, + "Drawing Command" : true, + "Drawing X" : false, + "Drawing Y" : false, "Error" : false, "Karaoke Template" : true, "Karaoke Variable" : true, @@ -251,9 +255,14 @@ "Slashes" : false, "Tags" : true }, + "Underline": { + "Drawing Endpoint": true + }, "Brackets" : "rgb(20, 50, 255)", "Comment" : "rgb(0,0,0)", - "Drawing" : "rgb(0,0,0)", + "Drawing Command" : "rgb(0,0,0)", + "Drawing X" : "rgb(90,40,40)", + "Drawing Y" : "rgb(40,90,40)", "Error" : "rgb(200, 0, 0)", "Karaoke Template" : "rgb(128, 0, 192)", "Karaoke Variable" : "rgb(128, 0, 192)", diff --git a/src/preferences.cpp b/src/preferences.cpp index f6320dd75..e176ac58e 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -253,7 +253,11 @@ void Interface_Colours(wxTreebook *book, Preferences *parent) { p->OptionAdd(syntax, _("Background"), "Colour/Subtitle/Background"); p->OptionAdd(syntax, _("Normal"), "Colour/Subtitle/Syntax/Normal"); p->OptionAdd(syntax, _("Comments"), "Colour/Subtitle/Syntax/Comment"); - p->OptionAdd(syntax, _("Drawings"), "Colour/Subtitle/Syntax/Drawing"); + p->OptionAdd(syntax, _("Drawing Commands"), "Colour/Subtitle/Syntax/Drawing Command"); + p->OptionAdd(syntax, _("Drawing X Coords"), "Colour/Subtitle/Syntax/Drawing X"); + p->OptionAdd(syntax, _("Drawing Y Coords"), "Colour/Subtitle/Syntax/Drawing Y"); + p->OptionAdd(syntax, _("Underline Spline Endpoints"), "Colour/Subtitle/Syntax/Underline/Drawing Endpoint"); + p->CellSkip(syntax); p->OptionAdd(syntax, _("Brackets"), "Colour/Subtitle/Syntax/Brackets"); p->OptionAdd(syntax, _("Slashes and Parentheses"), "Colour/Subtitle/Syntax/Slashes"); p->OptionAdd(syntax, _("Tags"), "Colour/Subtitle/Syntax/Tags"); diff --git a/src/subs_edit_ctrl.cpp b/src/subs_edit_ctrl.cpp index 9d1fc6a01..dbacce391 100644 --- a/src/subs_edit_ctrl.cpp +++ b/src/subs_edit_ctrl.cpp @@ -138,7 +138,10 @@ SubsTextEditCtrl::SubsTextEditCtrl(wxWindow* parent, wxSize wsize, long style, a OPT_SUB("Subtitle/Edit Box/Font Size", &SubsTextEditCtrl::SetStyles, this); Subscribe("Normal"); Subscribe("Comment"); - Subscribe("Drawing"); + Subscribe("Drawing Command"); + Subscribe("Drawing X"); + Subscribe("Drawing Y"); + OPT_SUB("Colour/Subtitle/Syntax/Underline/Drawing Endpoint", &SubsTextEditCtrl::SetStyles, this); Subscribe("Brackets"); Subscribe("Slashes"); Subscribe("Tags"); @@ -230,7 +233,13 @@ void SubsTextEditCtrl::SetStyles() { namespace ss = agi::ass::SyntaxStyle; SetSyntaxStyle(ss::NORMAL, font, "Normal", default_background); SetSyntaxStyle(ss::COMMENT, font, "Comment", default_background); - SetSyntaxStyle(ss::DRAWING, font, "Drawing", default_background); + SetSyntaxStyle(ss::DRAWING_CMD, font, "Drawing Command", default_background); + SetSyntaxStyle(ss::DRAWING_X, font, "Drawing X", default_background); + SetSyntaxStyle(ss::DRAWING_Y, font, "Drawing Y", default_background); + SetSyntaxStyle(ss::DRAWING_ENDPOINT_X, font, "Drawing X", default_background); + SetSyntaxStyle(ss::DRAWING_ENDPOINT_Y, font, "Drawing Y", default_background); + StyleSetUnderline(ss::DRAWING_ENDPOINT_X, OPT_GET("Colour/Subtitle/Syntax/Underline/Drawing Endpoint")->GetBool()); + StyleSetUnderline(ss::DRAWING_ENDPOINT_Y, OPT_GET("Colour/Subtitle/Syntax/Underline/Drawing Endpoint")->GetBool()); SetSyntaxStyle(ss::OVERRIDE, font, "Brackets", default_background); SetSyntaxStyle(ss::PUNCTUATION, font, "Slashes", default_background); SetSyntaxStyle(ss::TAG, font, "Tags", default_background); diff --git a/tests/tests/syntax_highlight.cpp b/tests/tests/syntax_highlight.cpp index ea3a2cc63..6b10c10b8 100644 --- a/tests/tests/syntax_highlight.cpp +++ b/tests/tests/syntax_highlight.cpp @@ -74,14 +74,47 @@ TEST(lagi_syntax, spellcheck) { } TEST(lagi_syntax, drawing) { - tok_str("incorrect{\\p1}m 10 10{\\p}correct", false, + tok_str("incorrect{\\clip(m 10 10 l 20 20 c)\\p1}m 10 10 b 0 0 0 100 100 0{\\p}correct", false, expect_style(ss::SPELLING, 9u); expect_style(ss::OVERRIDE, 1u); expect_style(ss::PUNCTUATION, 1u); + expect_style(ss::TAG, 4u); + expect_style(ss::PUNCTUATION, 1u); + expect_style(ss::DRAWING_CMD, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_X, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_Y, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_CMD, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_X, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_Y, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_CMD, 1u); + expect_style(ss::PUNCTUATION, 2u); expect_style(ss::TAG, 1u); expect_style(ss::PARAMETER, 1u); expect_style(ss::OVERRIDE, 1u); - expect_style(ss::DRAWING, 7u); + expect_style(ss::DRAWING_CMD, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_X, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_Y, 2u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_CMD, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_X, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_Y, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_X, 1u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_Y, 3u); + expect_style(ss::NORMAL, 1u); + expect_style(ss::DRAWING_ENDPOINT_X, 4u); + expect_style(ss::DRAWING_ENDPOINT_Y, 1u); expect_style(ss::OVERRIDE, 1u); expect_style(ss::PUNCTUATION, 1u); expect_style(ss::TAG, 1u); diff --git a/tests/tests/word_split.cpp b/tests/tests/word_split.cpp index 00ba82fcd..8ed0ff83f 100644 --- a/tests/tests/word_split.cpp +++ b/tests/tests/word_split.cpp @@ -108,12 +108,12 @@ TEST(lagi_word_split, drawing) { SplitWords(text, tokens); - ASSERT_EQ(15u, tokens.size()); + ASSERT_EQ(17u, tokens.size()); EXPECT_EQ(dt::WORD, tokens[0].type); EXPECT_EQ(dt::WORD, tokens[2].type); - EXPECT_EQ(dt::WORD, tokens[14].type); + EXPECT_EQ(dt::WORD, tokens[16].type); - EXPECT_EQ(dt::DRAWING, tokens[8].type); + EXPECT_EQ(dt::DRAWING_CMD, tokens[8].type); } TEST(lagi_word_split, unclosed_ovr) {