Leander Mihm
Leander Mihm

Reputation: 106

Rounded corners in subtitle (Advanced Substation Alpha [.ass])

Is it possible to have rounded corners with the BorderStyle 4 in .ass (Advanced Substation Alpha)? I only found out that the BorderStyle 4 exists, because I was looking at this stackoverflow. Is there any good and complete documentation of the Advanced Substation Alpha format?

I'm currently using the following configuration:

BorderStyle=4 Outline=10

Preview of the current state

Upvotes: 4

Views: 1128

Answers (1)

Jukizuka
Jukizuka

Reputation: 296

You need to use software called Aegisub and install dependency called ILL Shapery. It's recommended to download it via DepCtrl (Dependency Control), but dependency manager isn't built-in into standard Aegisub (available at aegisub.org), so I recommend installing an Arch's fork that has DepCtrl built-in (if you're running Windows).

Manual way

When you open Aegisub you need to download ILL Shapery like so:

  1. Click on Automation tab (it is located at top)
  2. Hover over DependencyControl and click Install Script option (loading can take a while)
  3. In Automation Scripts field, select Shapery v2.5.6 (or any current version) and leave Modules field empty
  4. Click OK and after it installs click Close
  5. Click on Automation tab and Automation... field (on top with blue icon)
  6. And in this Automation Manager click Rescan Autoload Dir and then close this pop-up window

Now select all subtitles and duplicate them (ctrl+A, right click, click on Duplicate) and the duplicated lines will be selected, so for now mark all duplicate lines as "comments" (so that they still appear in script but won't show on screen, but will allow us later show text when we finish our rounded corners in border).

Select all uncommented (visible) lines and remove their border (click Alt+3 and change alpha to 255) and shadow (click Alt+4 and change alpha to 255), and lastly change their primary color (Alt+1) to black (&H000000&).

  • Automation → : Shapery macros : → Shape expand
  • Automation → : Shapery macros : → Shape bounding box
  • Automation → Shapery → Offsetting
    • Stroke Weight: 10
    • Corner Style: Miter
    • Align Stroke: Outside
  • Automation → Shapery → Utilities
    • Corner: Rounded
    • Radius: as you want (for example: 15)
    • Rounding: Absolute Then click Corners

Now you can uncomment initial lines (those containing text) and change alpha of their border and shadow to 255. And change layer number of those lines with text to 10 or higher (just to be sure that they're displayed above border).

You should be left with something like this: img1

Scale and rounding of corners might be different due to the fact of using different PlayResX and PlayResY headers in [ScriptInfo] section.

It's almost perfect, it just is slighty misaligned visually in y-axis (in my opinion), so we need to position border about 1-2px higher.

To do it you need to select lines that have make borders (they have vector drawing instead of text) and while selecting them do:

  • Automation → Shapery → Transform
    • X axis: 0
    • Y axis: -2
    • Angle: 0
    • Hor. %: 100
    • Ver. %: 100 Then click OK

And now you should have the effect you wanted to achive: effect

Note: To make process easier you can 'chain' macros using AegisubChain macro and then only call single macro (instead of bunch of them) to do everything.

Programmatic way (added Dec 21 2024)

To achieve same thing using code, you need to use Aegisub-cli . Below are step-by-step instructions for setting it up on Windows and Linux (Ubuntu).

1. Download Arch1t3cht's Aegisub, Aegisub-cli & DepCtrl

Windows:

Download the Aegisub installer for Windows (Windows.MSVC.Release.wx.master.-.installer.zip) from the latest release (currently 'feature 12').

Download the prebuilt binary of Aegisub-cli from the latest release.

Place aegisub-cli.exe in the path/to/aegisub/Aegisub directory, right next to Aegisub.exe. So by default it will be C:\Program Files\Aegisub\aegisub-cli.exe.

Linux (Ubuntu 22.04) tested on WSL2:

# Install necessary dependencies
sudo apt-get update && sudo apt-get install -y \
python3-pip git cmake pkg-config ninja-build build-essential \
libx11-dev libwxgtk3.0-gtk3-dev libfreetype6-dev pkg-config \
libfontconfig1-dev libass-dev libasound2-dev libffms2-dev intltool \
libboost-all-dev libhunspell-dev libuchardet-dev libpulse-dev \
libopenal-dev libxxhash-dev nasm liblua5.1-0-dev luarocks \
libcurl4-gnutls-dev

# Install Meson
python3 -m pip install --upgrade pip setuptools
sudo pip3 install meson
export PATH="$PATH:~/.local/bin"

# Build Arch1t3cht's Aegisub
cd ~
git clone https://github.com/arch1t3cht/Aegisub.git -b feature_12
cd Aegisub/
meson setup build --prefix=/usr --buildtype=release
meson compile -C build
cd build
sudo ninja install

# Set up `Automation 4` modules for Aegisub
mkdir -p ~/.aegisub/automation
cd ~/.aegisub/automation

git clone https://github.com/TypesettingTools/DependencyControl.git
git clone https://github.com/TypesettingTools/YUtils.git
git clone https://github.com/arch1t3cht/ffi-experiments.git

cd DependencyControl
git checkout v0.6.3-alpha
cd ../ffi-experiments
sudo luarocks install moonscript
meson setup build -Ddefault_library=static
meson compile -C build
cd ..

mkdir -p autoload include/l0 include/BM/BadMutex include/PT/PreciseTimer include/DM/DownloadManager

mv DependencyControl/modules/DependencyControl DependencyControl/modules/DependencyControl.moon include/l0/
mv DependencyControl/macros/l0.DependencyControl.Toolbox.moon autoload/
mv YUtils/src/Yutils.lua include/Yutils.lua
mv ffi-experiments/build/requireffi include/
mv ffi-experiments/build/bad-mutex/libBadMutex.so* include/BM/BadMutex/
mv ffi-experiments/build/bad-mutex/BadMutex.lua include/BM/
mv ffi-experiments/build/precise-timer/libPreciseTimer.so* include/PT/PreciseTimer/
mv ffi-experiments/build/precise-timer/PreciseTimer.lua include/PT/
mv ffi-experiments/build/download-manager/libDownloadManager.so* include/DM/DownloadManager/
mv ffi-experiments/build/download-manager/DownloadManager.lua include/DM/
rm -rf DependencyControl/ ffi-experiments/ YUtils/

sudo luarocks install luajson

# Install Aegisub-cli
cd ~
git clone https://github.com/Myaamori/aegisub-cli
cd aegisub-cli

python3 -m pip install meson==0.62 # Downgrade Meson due to sandbox violation (known issue)

meson --prefix=/usr --buildtype=release build
meson compile -C build src/aegisub-cli

sudo mv build/src/aegisub-cli /usr/bin/
sudo mv build/src/libresrc/libresrc.a /usr/lib/
sudo mv build/src/libresrc/default_config.h /usr/include/

2. Download ILL and Clipper modules (by downloading Shapery, which will resolve them automatically)

Open Aegisub and do the following:

  1. Click on Automation tab (it is located at top)
  2. Hover over DependencyControl and click Install Script option (loading can take a while)
  3. In Automation Scripts field, select Shapery v2.5.6 (or any current version) and leave Modules field empty
  4. Click OK and after it installs click Close
  5. Click on Automation tab and Automation... field (on top with blue icon)
  6. And in this Automation Manager click Rescan Autoload Dir and then close this pop-up window

3. Create macro for creating rounded borders

I created a simple macro using the ILL module. To use it, you need to create a file named jz.RoundedBorders.lua in the/Aegisub/automation/autoload directory (macros in this location are automatically loaded when Aegisub starts). The default paths for this directory are:

  • Linux (Ubuntu 22.04): ~/.aegisub/automation/autoload/jz.RoundedBorders.lua
  • Windows: C:\Program Files\Aegisub\automation\autoload\jz.RoundedBorders.lua

And then paste the following Lua script:

script_name = "Create Rounded Border"
script_description = "Creates rounded borders for selected subtitles"
script_version = "0.1.0"
script_author = "Jukizuka"

local haveDepCtrl, DependencyControl = pcall(require, "l0.DependencyControl")

if haveDepCtrl then
    local depCtrl = DependencyControl({
        feed = "https://raw.githubusercontent.com/TypesettingTools/ILL-Aegisub-Scripts/main/DependencyControl.json",
        { "ILL.ILL" }
    })

    ILL = depCtrl:requireModules()
else
    ILL = require("ILL.ILL")
end


local Ass, Line, Path, Table = ILL.Ass, ILL.Line, ILL.Path, ILL.Table

function CreateRoundedBorders(bboxOffset, roundingRadius, transformY, borderColor, borderAlpha)
    local miterLimit, arcTolerance = 2, 0.25

    return function(sub, sel, activeLine)
        local ass = Ass(sub, sel, activeLine, true)

        for line, s, i, n in ass:iterSel() do
            ass:progressLine(s, i, n)
            Line.extend(ass, line) -- Populate line table

            -- Create top subtitle layer (text)
            local topLayer = Table.copy(line)
            topLayer.layer = topLayer.layer + 1
            ass:setLine(topLayer, s)

            -- Create bottom subtitle layer (rounded border)
            Line.callBackExpand(ass, line, nil, function(line)
                local bottomLayer = Table.copy(line)

                -- Create bounding box
                local boundingBox = Path(bottomLayer.shape):boundingBox()["assDraw"]
                local extendedBoundingBox = Path(boundingBox):offset(bboxOffset, "miter", "polygon", miterLimit, arcTolerance)

                -- Round bounding box and move it downward
                local roundedPath = Path.RoundingPath(
                    extendedBoundingBox:export(), roundingRadius, false, "Rounded", "Absolute"
                ):move(0, transformY)

                -- Set shape and border color
                bottomLayer.shape = roundedPath:export()

                bottomLayer.tags:insert({ { "c", borderColor } })
                bottomLayer.tags:insert({ { "1a", borderAlpha } })

                return ass:insertLine(bottomLayer, s)
            end)
        end

        return ass:getNewSelection()
    end
end

function Gui(sub, sel, activeLine)
    local dialogConfig =
    {
        { x = 0, y = 0, width = 1, height = 1, class = "label",   label = "Offset: " },
        { x = 1, y = 0, width = 1, height = 1, class = "intedit", name = "offset" },
        { x = 0, y = 1, width = 1, height = 1, class = "label",   label = "Radius: " },
        { x = 1, y = 1, width = 1, height = 1, class = "intedit", name = "radius" },
        { x = 0, y = 2, width = 1, height = 1, class = "label",   label = "Transform Y: " },
        { x = 1, y = 2, width = 1, height = 1, class = "intedit", name = "transformY" },
        { x = 0, y = 3, width = 1, height = 1, class = "label",   label = "Border Color: " },
        { x = 1, y = 3, width = 1, height = 1, class = "textbox", name = "borderColor",    text = "&H000000&" },
        { x = 0, y = 3, width = 1, height = 1, class = "label",   label = "Border Alpha: " },
        { x = 1, y = 3, width = 1, height = 1, class = "textbox", name = "borderAlpha",    text = "&H00&" }

    }

    local pressed, res = aegisub.dialog.display(dialogConfig)
    if not pressed then aegisub.cancel() end

    return CreateRoundedBorders(res.offset, res.radius, res.transformY, res.borderColor, res.borderAlpha)(sub, sel, activeLine)
end

aegisub.register_macro(script_name, script_description, Gui)

4. Example usage of aegisub-cli

Windows Example:

aegisub-cli.exe --dialog "{\"button\": 0, \"values\": {\"offset\": 20, \"radius\": 20, \"transformY\": -1, \"borderColor\": \"&H000000&\", \"borderAlpha": \"&H00\"}}" --automation jz.RoundedBorders.lua input.ass output.ass "Create Rounded Border"

Linux Example:

aegisub-cli --dialog '{"button": 0, "values": {"offset": 20, "radius": 20, "transformY": -1, "borderColor": "&H000000&", "borderAlpha": "&H00"}}' --automation jz.RoundedBorders.lua input.ass output.ass "Create Rounded Border"

Universal example (using Python):

import json
import subprocess
import shutil

aegicli_path = shutil.which("aegisub-cli")

bboxOffset = 20
roundingRadius = 20
yTransform = -1
border_color = "#000000"
border_alpha = 255

input_file = "input.ass"
output_file = "output.ass"


def ssa_bgr(color: str) -> str:
    r, g, b = bytes.fromhex(color.lstrip("#"))
    return f"&H{b:02X}{g:02X}{r:02X}&"


def ssa_alpha(alpha: int) -> str:
    return f"&H{255 - alpha:02X}"


dialog_json = json.dumps(
    {"button": 0,
     "values": {
         "offset": 20,
         "radius": 20,
         "transformY": -1,
         "borderColor": ssa_bgr(border_color),
         "borderAlpha": ssa_alpha(border_alpha),
     }}
)

args = [
    aegicli_path,
    "--dialog", dialog_json,
    "--automation", "jz.RoundedBorders.lua",
    input_file, output_file,
    "Create Rounded Border"
]

subprocess.run(args)

For example, if there are 5 lines to be processed, the output should look like this:

./src/auto4_lua_dialog.cpp (397): I 13:23:19.996 <agi/auto4_lua_dialog     > [Automation4::LuaDialog::LuaReadBack]  Pushing OK
alog::LuaReadBack]  Pushing OK
../src/dialog_progress.cpp (52): I 13:24:57.255 <agi/dialog_progress      > [DialogProgressSink::SetProgress]  Progress: 20%
../src/dialog_progress.cpp (52): I 13:24:57.260 <agi/dialog_progress      > [DialogProgressSink::SetProgress]  Progress: 40%
../src/dialog_progress.cpp (52): I 13:24:57.263 <agi/dialog_progress      > [DialogProgressSink::SetProgress]  Progress: 60%
../src/dialog_progress.cpp (52): I 13:24:57.266 <agi/dialog_progress      > [DialogProgressSink::SetProgress]  Progress: 80%
../src/dialog_progress.cpp (52): I 13:24:57.270 <agi/dialog_progress      > [DialogProgressSink::SetProgress]  Progress: 100%

Upvotes: 1

Related Questions