Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion helpers/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,27 @@ def resolve_path(maybe_path: str, base: Path) -> Path:
return (base / p).resolve()


def _escape_filter_value(s: str) -> str:
r"""Make a path safe inside an ffmpeg filtergraph option value.

In a filtergraph, ':' separates options and '\' is special, so a Windows
path (C:\Users\me\master.srt) breaks parsing. Forward-slash the separators
(ffmpeg accepts them on Windows), then escape the drive colon and any quote.
Pure string transform — host-independent, so it's the unit the tests pin.
"""
return s.replace("\\", "/").replace(":", r"\:").replace("'", r"\'")


def escape_subtitles_path(path: Path) -> str:
r"""Resolve `path` and escape it for ffmpeg's `subtitles=` filter option.

No-op-shaped on POSIX (paths there contain neither '\' nor a drive ':');
on Windows it turns C:\Users\me\master.srt into C\:/Users/me/master.srt so
the subtitles filter parses instead of erroring.
"""
return _escape_filter_value(str(path.resolve()))


# -------- HDR → SDR tone mapping (HLG / PQ sources) --------------------------
#
# iPhone defaults to HLG HDR in Rec.2020 (and many mirrorless cameras ship PQ).
Expand Down Expand Up @@ -537,7 +558,7 @@ def build_final_composite(

# Subtitles LAST — Rule 1
if has_subs:
subs_abs = str(subtitles_path.resolve()).replace(":", r"\:").replace("'", r"\'")
subs_abs = escape_subtitles_path(subtitles_path)
filter_parts.append(
f"{current}subtitles='{subs_abs}':force_style='{SUB_FORCE_STYLE}'[outv]"
)
Expand Down
41 changes: 41 additions & 0 deletions tests/test_render_subtitles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
r"""Regression test for cross-platform subtitles-path escaping in render.py.

Bug: render.py built the ffmpeg `subtitles=` filter from an absolute path that,
on Windows, looks like C:\Users\me\master.srt. The filter escaped ':' and "'"
but left '\' separators raw, so ffmpeg's filtergraph parser mangled the path and
subtitle burning failed on Windows. Fix: _escape_filter_value forward-slashes the
separators and escapes the drive colon; escape_subtitles_path applies it to the
resolved path. These tests pin the pure transform for Windows and POSIX inputs
and pass on any host.
"""
import sys
import unittest
from pathlib import Path

HELPERS_DIR = Path(__file__).resolve().parent.parent / "helpers"
sys.path.insert(0, str(HELPERS_DIR))
import render # noqa: E402


class EscapeFilterValueTest(unittest.TestCase):
def test_windows_path_forward_slashed_and_colon_escaped(self):
out = render._escape_filter_value(r"C:\Users\me\AppData\Local\Temp\master.srt")
# Drive colon escaped, all path separators forward-slashed. The only
# backslash that survives is the one escaping the colon.
self.assertEqual(out, r"C\:/Users/me/AppData/Local/Temp/master.srt")
for i, ch in enumerate(out): # every ':' is escaped (preceded by a backslash)
if ch == ":":
self.assertTrue(i > 0 and out[i - 1] == "\\")

def test_posix_path_unchanged(self):
p = "/home/me/edit/master.srt"
self.assertEqual(render._escape_filter_value(p), p)

def test_single_quote_escaped(self):
out = render._escape_filter_value("/tmp/it's/master.srt")
self.assertIn(r"\'", out)
self.assertNotIn("'", out.replace(r"\'", "")) # no unescaped quote remains


if __name__ == "__main__":
unittest.main()