feat(clip): crop a clip to a frame drawn on the picture, and give the clip panel the room it needs
An optional crop in clip mode: a Crop toggle puts a frame on the picture (free, 16:9, 9:16 or 1:1; the whole picture stays the default). The frame is sent in fractions of the box it was drawn in, with that box's shape; the worker fits the real picture into the box by its display aspect (sample aspect and rotation aware), drops the part over the bars, and crops first in the filter chain. A frame covering the whole box is no crop at all, and one off the picture is refused by name. The crop frame is a ui primitive (CropFrame) shared with the video editor, keyboard-operable. YouTube: the wide side panel shuts while clipping and comes back after; maximised, the clip panel docks in the stage. Plex: the picture sits above the clip panel so the frame lies on it.
This commit is contained in:
@@ -126,6 +126,99 @@ def test_hdr_is_read_from_the_first_video_stream(transfer, hdr):
|
||||
assert clipmod.is_hdr(data) is hdr
|
||||
|
||||
|
||||
# --- the crop ----------------------------------------------------------------------------------------
|
||||
|
||||
_FRAME = {"x": 0.5, "y": 0.25, "w": 0.25, "h": 0.5, "aspect": 16 / 9}
|
||||
|
||||
|
||||
def test_a_crop_frame_is_canonical_and_a_frame_over_the_whole_box_is_no_crop():
|
||||
a = clipmod.normalize("plex", "42", 0, 10, crop={**_FRAME, "x": 0.50004})
|
||||
b = clipmod.normalize("plex", "42", 0, 10, crop=_FRAME)
|
||||
assert a["crop"] == b["crop"] == {"x": 0.5, "y": 0.25, "w": 0.25, "h": 0.5, "aspect": 1.7778}
|
||||
assert clipmod.sig(a) == clipmod.sig(b)
|
||||
whole = clipmod.normalize(
|
||||
"plex", "42", 0, 10, crop={"x": 0, "y": 0, "w": 1, "h": 1, "aspect": 1}
|
||||
)
|
||||
assert whole == clipmod.normalize("plex", "42", 0, 10)
|
||||
# A far edge a hair past the box (the client's rounding) is the box's edge.
|
||||
assert clipmod.normalize("plex", "42", 0, 10, crop={**_FRAME, "w": 0.5004})["crop"]["w"] == 0.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"frame",
|
||||
[
|
||||
{**_FRAME, "w": 0.05},
|
||||
{**_FRAME, "h": 0.09},
|
||||
{**_FRAME, "x": -0.1},
|
||||
{**_FRAME, "x": 0.8},
|
||||
{**_FRAME, "y": 0.6},
|
||||
{**_FRAME, "aspect": 0},
|
||||
{**_FRAME, "aspect": 9},
|
||||
{**_FRAME, "x": float("nan")},
|
||||
{"x": 0.1, "y": 0.1, "w": 0.5},
|
||||
{**_FRAME, "h": "half"},
|
||||
],
|
||||
)
|
||||
def test_a_crop_frame_it_cannot_cut_is_refused(frame):
|
||||
with pytest.raises(clipmod.ClipError) as e:
|
||||
clipmod.normalize("plex", "42", 0, 10, crop=frame)
|
||||
assert e.value.reason == "bad_crop"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("stream", "dar"),
|
||||
[
|
||||
({"width": 1920, "height": 1080}, 16 / 9),
|
||||
# An anamorphic DVD: 720×480 stored, shown 16:9.
|
||||
({"width": 720, "height": 480, "sample_aspect_ratio": "32:27"}, 16 / 9),
|
||||
({"width": 640, "height": 480, "sample_aspect_ratio": "0:1"}, 4 / 3),
|
||||
# A phone video stored landscape, shown upright.
|
||||
({"width": 1280, "height": 720, "side_data_list": [{"rotation": -90}]}, 9 / 16),
|
||||
({"width": 1280, "height": 720, "tags": {"rotate": "270"}}, 9 / 16),
|
||||
({"width": 1280, "height": 720, "side_data_list": [{"rotation": 180}]}, 16 / 9),
|
||||
({"width": None, "height": 720}, None),
|
||||
],
|
||||
)
|
||||
def test_the_display_aspect_is_the_picture_as_a_player_shows_it(stream, dar):
|
||||
got = clipmod.display_aspect(
|
||||
{"streams": [{"codec_type": "audio"}, {"codec_type": "video", **stream}]}
|
||||
)
|
||||
assert got == pytest.approx(dar) if dar else got is None
|
||||
|
||||
|
||||
def _crop_args(f: str) -> list[float]:
|
||||
# crop=trunc(iw*W/2)*2:trunc(ih*H/2)*2:trunc(iw*X/2)*2:trunc(ih*Y/2)*2 → [W, H, X, Y]
|
||||
assert f.startswith("crop=")
|
||||
return [float(p.split("*")[1].split("/")[0]) for p in f[5:].split(":")]
|
||||
|
||||
|
||||
def test_a_frame_on_a_picture_that_fills_its_box_is_cut_as_drawn():
|
||||
assert _crop_args(clipmod.crop_filter(_FRAME, 16 / 9)) == pytest.approx([0.25, 0.5, 0.5, 0.25])
|
||||
|
||||
|
||||
def test_a_frame_drawn_in_a_16_9_box_over_a_4_3_picture_maps_onto_the_picture():
|
||||
# The 4:3 picture fills 3/4 of the box's width from 1/8 in: a frame over its right half's middle.
|
||||
frame = {"x": 0.5, "y": 0.25, "w": 0.1875, "h": 0.5, "aspect": 16 / 9}
|
||||
assert _crop_args(clipmod.crop_filter(frame, 4 / 3)) == pytest.approx([0.25, 0.5, 0.5, 0.25])
|
||||
|
||||
|
||||
def test_the_part_of_a_frame_over_a_bar_is_dropped_and_a_frame_on_a_bar_alone_is_no_cut():
|
||||
over = {"x": 0.0, "y": 0.0, "w": 0.25, "h": 0.5, "aspect": 16 / 9}
|
||||
w, h, x, y = _crop_args(clipmod.crop_filter(over, 4 / 3))
|
||||
assert (w, h, x, y) == pytest.approx([1 / 6, 0.5, 0, 0], abs=1e-6)
|
||||
assert clipmod.crop_filter({**over, "w": 0.13}, 4 / 3) is None
|
||||
# A letterboxed film: the frame over the whole box is the whole picture.
|
||||
whole = {"x": 0.0, "y": 0.0, "w": 1.0, "h": 1.0, "aspect": 16 / 9}
|
||||
assert clipmod.crop_filter(whole, 2.39) == ""
|
||||
|
||||
|
||||
def test_the_crop_comes_first_so_the_tonemap_and_the_scale_work_on_what_it_keeps():
|
||||
f = clipmod.crop_filter(_FRAME, 16 / 9)
|
||||
vf = _cmd(hdr=True, crop=f)[_cmd(hdr=True, crop=f).index("-vf") + 1]
|
||||
assert vf.startswith(f + ",") and vf.index("tonemap") < vf.index("min(1080,ih)")
|
||||
assert "crop" not in _cmd()[_cmd().index("-vf") + 1]
|
||||
|
||||
|
||||
# --- the route ---------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -351,6 +444,45 @@ def test_a_plex_clip_is_cut_from_the_library_file_with_its_audio_track(
|
||||
assert asset.rel_path.startswith(".edits/") and asset.rel_path.endswith(".mp4")
|
||||
|
||||
|
||||
def test_a_cropped_plex_clip_cuts_the_frame_from_the_picture_and_is_its_own_clip(
|
||||
db, make_user, as_user, plex_episode, worker
|
||||
):
|
||||
worker["probe"] = {
|
||||
"streams": [
|
||||
{"codec_type": "video", "width": 720, "height": 480, "sample_aspect_ratio": "32:27"}
|
||||
]
|
||||
}
|
||||
c = as_user(make_user())
|
||||
plain = _clip(c, source="plex", ref="777").json()
|
||||
job = _clip(c, source="plex", ref="777", crop=_FRAME).json()
|
||||
assert job["asset"]["id"] != plain["asset"]["id"]
|
||||
assert job["edit_spec"]["crop"]["w"] == 0.25
|
||||
job, asset = worker["run"](job)
|
||||
assert (job.status, asset.status) == ("done", "ready")
|
||||
vf = worker["cmd"][worker["cmd"].index("-vf") + 1]
|
||||
# Stored to four decimals, the box's 16:9 is a hair off the file's: well under a pixel.
|
||||
assert _crop_args(vf.split(",")[0]) == pytest.approx([0.25, 0.5, 0.5, 0.25], abs=1e-4)
|
||||
|
||||
|
||||
def test_a_crop_frame_that_misses_the_picture_fails_and_says_so(
|
||||
db, make_user, as_user, plex_episode, worker
|
||||
):
|
||||
# A 4:3 file, a frame drawn over its pillar bar in a 16:9 box.
|
||||
worker["probe"] = {"streams": [{"codec_type": "video", "width": 640, "height": 480}]}
|
||||
frame = {"x": 0.0, "y": 0.0, "w": 0.12, "h": 0.5, "aspect": 16 / 9}
|
||||
job = _clip(as_user(make_user()), source="plex", ref="777", crop=frame).json()
|
||||
job, asset = worker["run"](job)
|
||||
assert (job.status, asset.status) == ("error", "error")
|
||||
assert "crop" in job.error
|
||||
assert "cmd" not in worker
|
||||
|
||||
|
||||
def test_a_crop_frame_the_route_cannot_cut_says_why(db, make_user, as_user, yt_video):
|
||||
r = _clip(as_user(make_user()), source="youtube", ref=yt_video, crop={**_FRAME, "w": 0.01})
|
||||
assert r.status_code == 422
|
||||
assert r.json()["detail"] == {"code": "clip", "reason": "bad_crop"}
|
||||
|
||||
|
||||
def test_a_plex_clip_of_a_track_the_file_does_not_have_fails_and_says_so(
|
||||
db, make_user, as_user, plex_episode, worker
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user