Skip to content

Commit 867e356

Browse files
committed
Support recursive video-crop and zoom-pan
Based on occivink#70 - Support recursive `video-crop` and `zoom-pan` - zoom-pan (soft) can now be toggled - Added remove-crop: - `remove-crop [{type}]` - Removes all filters starting with delogo. If `{type}` is specified it removes only filters of that type (hard, delogo, soft). - `remove-crop all [{type}]` - Removes all filters starting with specified type. If no type is specified it removes all filters. - `remove-crop all order` - Removes filters starting with the most recently added.
1 parent 567f794 commit 867e356

File tree

3 files changed

+199
-36
lines changed

3 files changed

+199
-36
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ UX largely inspired by [this script](https://github.com/aidanholm/mpv-easycrop),
1616

1717
Press the binding to enter crop mode. Click once to define the first corner of the cropped zone, click a second time to define the second corner.
1818

19-
Note that [hardware decoding is in general not compatible with filters](https://mpv.io/manual/master/#options-hwdec), and will therefore not work with this script.
20-
2119
# encode.lua
2220

2321
**You need ffmpeg in your PATH (or in the same folder as mpv) for this script to work.**

input.conf

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ alt+c script-message-to crop start-crop soft
66
# delogo mode can be used like so
77
l script-message-to crop start-crop delogo
88
# remove the crop
9-
d vf del -1
9+
# `remove-crop [{type}]` - Removes all filters starting with delogo. If `{type}` is specified it removes only filters of that type (hard, delogo, soft).
10+
# `remove-crop all [{type}]` - Removes all filters starting with specified type. If no type is specified it removes all filters.
11+
# `remove-crop all order` - Removes filters starting with the most recently added.
12+
d script-message-to crop remove-crop all order
1013

1114
# or use the ready-made "toggle" binding
1215
C script-message-to crop toggle-crop hard
1316

1417
# remove the soft zoom
15-
0 set video-pan-x 0; set video-pan-y 0; set video-zoom 0
18+
0 script-message-to crop remove-crop soft
1619

1720
# encode.lua
1821
# ============

scripts/crop.lua

Lines changed: 194 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
local opts = {
2-
mode = "hard", -- can be "hard" or "soft". If hard, apply a crop filter, if soft zoom + pan. Or a bonus "delogo" mode
2+
mode = "hard", -- can be "hard" or "soft". If hard, use video-crop, if soft use zoom + pan. Or a bonus "delogo" mode
33
draw_shade = true,
44
shade_opacity = "77",
55
draw_frame = false,
@@ -261,6 +261,11 @@ function draw_crop_zone()
261261
end
262262
end
263263

264+
-- history tables
265+
local recursive_crop = {}
266+
local recursive_zoom_pan = {}
267+
local remove_last_filter = {}
268+
264269
function crop_video(x1, y1, x2, y2)
265270
if active_mode == "soft" then
266271
local w = x2 - x1
@@ -271,34 +276,63 @@ function crop_video(x1, y1, x2, y2)
271276
local zoom = mp.get_property_number("video-zoom")
272277
local newZoom1 = math.log(dim.h * (2 ^ zoom) / (dim.h - dim.mt - dim.mb) / h) / math.log(2)
273278
local newZoom2 = math.log(dim.w * (2 ^ zoom) / (dim.w - dim.ml - dim.mr) / w) / math.log(2)
279+
280+
local newZoom = math.min(newZoom1, newZoom2)
281+
local newPanX = 0.5 - (x1 + w / 2)
282+
local newPanY = 0.5 - (y1 + h / 2)
283+
274284
mp.set_property("video-zoom", math.min(newZoom1, newZoom2))
275285
mp.set_property("video-pan-x", 0.5 - (x1 + w / 2))
276286
mp.set_property("video-pan-y", 0.5 - (y1 + h / 2))
287+
288+
table.insert(recursive_zoom_pan, {zoom = newZoom, panX = newPanX, panY = newPanY})
289+
290+
mp.set_property("video-zoom", newZoom)
291+
mp.set_property("video-pan-x", newPanX)
292+
mp.set_property("video-pan-y", newPanY)
293+
table.insert(remove_last_filter, "soft")
294+
277295
elseif active_mode == "hard" or active_mode == "delogo" then
278296
x1 = clamp(0, x1, 1)
279297
y1 = clamp(0, y1, 1)
280298
x2 = clamp(0, x2, 1)
281299
y2 = clamp(0, y2, 1)
282300
local vop = mp.get_property_native("video-out-params")
283-
local vf_table = mp.get_property_native("vf")
284-
local x = math.floor(x1 * vop.w + 0.5)
285-
local y = math.floor(y1 * vop.h + 0.5)
286-
local w = math.floor((x2 - x1) * vop.w + 0.5)
287-
local h = math.floor((y2 - y1) * vop.h + 0.5)
288-
if active_mode == "delogo" then
301+
if active_mode == "hard" then
302+
local w = x2 - x1
303+
local h = y2 - y1
304+
305+
table.insert(recursive_crop, {x = x1, y = y1, w = w, h = h})
306+
apply_video_crop()
307+
table.insert(remove_last_filter, "hard")
308+
309+
elseif active_mode == "delogo" then
310+
local vf_table = mp.get_property_native("vf")
311+
312+
local x, y, w, h = adjust_coordinates()
313+
314+
local x = math.floor((x + x1 * w) * vop.w + 0.5)
315+
local y = math.floor((y + y1 * h) * vop.h + 0.5)
316+
local w = math.floor(w * (x2 - x1) * vop.w + 0.5)
317+
local h = math.floor(h * (y2 - y1) * vop.h + 0.5)
318+
289319
-- delogo is a little special and needs some padding to function
290320
w = math.min(vop.w - 1, w)
291321
h = math.min(vop.h - 1, h)
292322
x = math.max(1, x)
293323
y = math.max(1, y)
324+
294325
if x + w == vop.w then w = w - 1 end
295326
if y + h == vop.h then h = h - 1 end
327+
328+
vf_table[#vf_table + 1] = {
329+
name="delogo",
330+
params= { x = tostring(x), y = tostring(y), w = tostring(w), h = tostring(h) }
331+
}
332+
333+
mp.set_property_native("vf", vf_table)
334+
table.insert(remove_last_filter, "delogo")
296335
end
297-
vf_table[#vf_table + 1] = {
298-
name=(active_mode == "hard") and "crop" or "delogo",
299-
params= { x = tostring(x), y = tostring(y), w = tostring(w), h = tostring(h) }
300-
}
301-
mp.set_property_native("vf", vf_table)
302336
end
303337
end
304338

@@ -346,6 +380,136 @@ function cancel_crop()
346380
end
347381
end
348382

383+
-- adjust coordinates based on previous values
384+
function adjust_coordinates()
385+
local x, y, w, h = 0, 0, 1, 1
386+
for _, crop in ipairs(recursive_crop) do
387+
x = x + w * crop.x
388+
y = y + h * crop.y
389+
w = w * crop.w
390+
h = h * crop.h
391+
end
392+
return x, y, w, h
393+
end
394+
395+
function apply_video_crop()
396+
local x, y, w, h = adjust_coordinates()
397+
398+
local vop = mp.get_property_native("video-out-params")
399+
local x = math.floor(x * vop.w + 0.5)
400+
local y = math.floor(y * vop.h + 0.5)
401+
local w = math.floor(w * vop.w + 0.5)
402+
local h = math.floor(h * vop.h + 0.5)
403+
404+
local video_crop = tostring(w) .."x".. tostring(h) .."+".. tostring(x) .."+".. tostring(y)
405+
mp.set_property_native("video-crop", video_crop)
406+
end
407+
408+
function remove_filter(vf_table, filter_name, filter_number)
409+
local filter_count = 0
410+
local remove_last = 0
411+
for i = 1, #vf_table do
412+
if vf_table[i].name == filter_name then
413+
filter_count = filter_count + 1
414+
remove_last = i
415+
end
416+
end
417+
if filter_count > 0 then
418+
table.remove(vf_table, remove_last)
419+
mp.set_property_native("vf", vf_table)
420+
mp.osd_message("Removed: #" .. tostring(filter_number or filter_count) .. " " .. filter_name)
421+
return true
422+
end
423+
return false
424+
end
425+
426+
function remove_video_crop(filter_number)
427+
if #recursive_crop > 0 then
428+
table.remove(recursive_crop)
429+
-- reapply each crop in the table
430+
apply_video_crop()
431+
if #recursive_crop == 0 then
432+
mp.set_property_native("video-crop", "")
433+
end
434+
mp.osd_message("Removed: #" .. tostring(filter_number or #recursive_crop + 1) .. " " .. "video-crop")
435+
return true
436+
end
437+
return false
438+
end
439+
440+
function remove_zoom_pan(filter_number)
441+
if #recursive_zoom_pan > 0 then
442+
table.remove(recursive_zoom_pan)
443+
if #recursive_zoom_pan > 0 then
444+
local lastZoomPan = recursive_zoom_pan[#recursive_zoom_pan]
445+
mp.set_property("video-zoom", lastZoomPan.zoom)
446+
mp.set_property("video-pan-x", lastZoomPan.panX)
447+
mp.set_property("video-pan-y", lastZoomPan.panY)
448+
else
449+
mp.set_property("video-zoom", 0)
450+
mp.set_property("video-pan-x", 0)
451+
mp.set_property("video-pan-y", 0)
452+
end
453+
mp.osd_message("Removed: #" .. tostring(filter_number or #recursive_zoom_pan + 1) .. " " .. "soft-crop")
454+
return true
455+
end
456+
return false
457+
end
458+
459+
-- remove an entry in 'remove_last_filter' at correct position to keep it in sync when 'remove_crop' and 'toggle_crop' are used in the same session
460+
function remove_last_filter_entry(filter_type)
461+
for i = #remove_last_filter, 1, -1 do
462+
if remove_last_filter[i] == filter_type then
463+
table.remove(remove_last_filter, i)
464+
break
465+
end
466+
end
467+
end
468+
469+
function remove_crop(mode, order)
470+
local vf_table = mp.get_property_native("vf")
471+
local total_filters = #remove_last_filter
472+
473+
-- 'remove-crop all order' removes all filters starting with most recently added
474+
if order == "order" then
475+
if total_filters == 0 then
476+
mp.osd_message("Nothing to remove")
477+
return
478+
end
479+
local last_filter = table.remove(remove_last_filter)
480+
if last_filter == "hard" then
481+
remove_video_crop(total_filters)
482+
elseif last_filter == "delogo" then
483+
remove_filter(vf_table, "delogo", total_filters)
484+
elseif last_filter == "soft" then
485+
remove_zoom_pan(total_filters)
486+
end
487+
else
488+
local modes = {"delogo", "hard", "soft"}
489+
if order == "hard" then
490+
modes = {"hard", "soft", "delogo"}
491+
elseif order == "soft" then
492+
modes = {"soft", "hard", "delogo"}
493+
end
494+
495+
for _, mode_name in ipairs(modes) do
496+
if not mode or mode == "all" or mode == mode_name then
497+
if mode_name == "delogo" and remove_filter(vf_table, "delogo") then
498+
remove_last_filter_entry("delogo")
499+
return
500+
elseif mode_name == "hard" and remove_video_crop() then
501+
remove_last_filter_entry("hard")
502+
return
503+
elseif mode_name == "soft" and remove_zoom_pan() then
504+
remove_last_filter_entry("soft")
505+
return
506+
end
507+
end
508+
end
509+
mp.osd_message("Nothing to remove")
510+
end
511+
end
512+
349513
function start_crop(mode)
350514
if active then return end
351515
if not mp.get_property_native("osd-dimensions") then return end
@@ -354,7 +518,7 @@ function start_crop(mode)
354518
return
355519
end
356520
local mode_maybe = mode or opts.mode
357-
if mode_maybe ~= 'soft' then
521+
if mode_maybe == "delogo" then
358522
local hwdec = mp.get_property("hwdec-current")
359523
if hwdec and hwdec ~= "no" and not string.find(hwdec, "-copy$") then
360524
msg.error("Cannot crop with hardware decoding active (see manual)")
@@ -387,27 +551,24 @@ function toggle_crop(mode)
387551
msg.error("Invalid mode value: " .. mode)
388552
end
389553
local toggle_mode = mode or opts.mode
390-
if toggle_mode == "soft" then return end -- can't toggle soft mode
391-
392-
local remove_filter = function()
393-
local to_remove = (toggle_mode == "hard") and "crop" or "delogo"
394-
local vf_table = mp.get_property_native("vf")
395-
if #vf_table > 0 then
396-
for i = #vf_table, 1, -1 do
397-
if vf_table[i].name == to_remove then
398-
for j = i, #vf_table-1 do
399-
vf_table[j] = vf_table[j+1]
400-
end
401-
vf_table[#vf_table] = nil
402-
mp.set_property_native("vf", vf_table)
403-
return true
404-
end
405-
end
406-
end
407-
return false
554+
555+
if toggle_mode == "soft" and not remove_zoom_pan() then
556+
start_crop(mode)
557+
elseif toggle_mode == "soft" then
558+
remove_last_filter_entry("soft")
559+
end
560+
561+
local vf_table = mp.get_property_native("vf")
562+
if toggle_mode == "delogo" and not remove_filter(vf_table, "delogo") then
563+
start_crop(mode)
564+
elseif toggle_mode == "delogo" then
565+
remove_last_filter_entry("delogo")
408566
end
409-
if not remove_filter() then
567+
568+
if toggle_mode == "hard" and not remove_video_crop() then
410569
start_crop(mode)
570+
elseif toggle_mode == "hard" then
571+
remove_last_filter_entry("hard")
411572
end
412573
end
413574

@@ -443,5 +604,6 @@ bindings_repeat[opts.up_fine] = movement_func(0, -opts.fine_movement)
443604
bindings_repeat[opts.down_fine] = movement_func(0, opts.fine_movement)
444605

445606

607+
mp.add_key_binding(nil, "remove-crop", remove_crop)
446608
mp.add_key_binding(nil, "start-crop", start_crop)
447609
mp.add_key_binding(nil, "toggle-crop", toggle_crop)

0 commit comments

Comments
 (0)