Skip to content

Commit 9837a3a

Browse files
Sneakpeakcssdexeonify
authored andcommitted
crop: Support recursive video-crop and zoom-pan
A more robust implementation courtesy of Sneakpeakcss: - Support recursive hard and soft crop - Support toggling soft crop (zoom-pan) - Add `remove-crop` script-message: - `remove-crop [{type}]` Remove all filters starting with delogo. If `{type}` is specified, remove only filters of that type (hard, delogo, soft). - `remove-crop all [{type}]` Remove all filters starting with specified type. If `{type}` is unspecified, remove all filters. - `remove-crop all order` Remove filters starting with the most recently added. Supersedes a2f3048, 479e687, b16b5ba Ref: Sneakpeakcss/mpv-scripts@fb7d5f6
1 parent 113cc6a commit 9837a3a

File tree

2 files changed

+182
-37
lines changed

2 files changed

+182
-37
lines changed

input.conf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ t script-binding seek_to/toggle-seeker #! Scripts > Seek to
117117
c script-message-to crop start-crop hard #! Scripts > Crop > Hard crop
118118
Alt+c script-message-to crop start-crop soft #! Scripts > Crop > Soft crop
119119
Ctrl+l script-message-to crop start-crop delogo #! Scripts > Crop > Delogo
120-
d script-message-to crop remove-crop #! Scripts > Crop > Remove crop
120+
d script-message-to crop remove-crop all order #! Scripts > Crop > Remove crop
121+
# script-message-to crop remove-crop soft #! Scripts > Crop > Remove soft crop
121122
$ script-binding skipsilence/toggle #! Scripts > SkipSilence > Toggle skipsilence
122123
# script-binding skipsilence/toggle-arnndn #! Scripts > SkipSilence > Toggle arnndn
123124
/ script-binding skipsilence/threshold-up #! Scripts > SkipSilence > Increase silence threshold

scripts/crop.lua

Lines changed: 180 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ function draw_crop_zone()
267267
end
268268
end
269269

270+
-- history tables
271+
local recursive_crop = {}
272+
local recursive_zoom_pan = {}
273+
local remove_last_filter = {}
274+
270275
function crop_video(x1, y1, x2, y2)
271276
if active_mode == "soft" then
272277
local w = x2 - x1
@@ -277,36 +282,57 @@ function crop_video(x1, y1, x2, y2)
277282
local zoom = mp.get_property_number("video-zoom")
278283
local newZoom1 = math.log(dim.h * (2 ^ zoom) / (dim.h - dim.mt - dim.mb) / h) / math.log(2)
279284
local newZoom2 = math.log(dim.w * (2 ^ zoom) / (dim.w - dim.ml - dim.mr) / w) / math.log(2)
280-
mp.set_property("video-zoom", math.min(newZoom1, newZoom2))
281-
mp.set_property("video-pan-x", 0.5 - (x1 + w / 2))
282-
mp.set_property("video-pan-y", 0.5 - (y1 + h / 2))
285+
286+
local newZoom = math.min(newZoom1, newZoom2)
287+
local newPanX = 0.5 - (x1 + w / 2)
288+
local newPanY = 0.5 - (y1 + h / 2)
289+
290+
table.insert(recursive_zoom_pan, {zoom = newZoom, panX = newPanX, panY = newPanY})
291+
mp.set_property("video-zoom", newZoom)
292+
mp.set_property("video-pan-x", newPanX)
293+
mp.set_property("video-pan-y", newPanY)
294+
table.insert(remove_last_filter, "soft")
295+
283296
elseif active_mode == "hard" or active_mode == "delogo" then
284297
x1 = clamp(0, x1, 1)
285298
y1 = clamp(0, y1, 1)
286299
x2 = clamp(0, x2, 1)
287300
y2 = clamp(0, y2, 1)
288301
local vop = mp.get_property_native("video-out-params")
289-
local x = math.floor(x1 * vop.w + 0.5)
290-
local y = math.floor(y1 * vop.h + 0.5)
291-
local w = math.floor((x2 - x1) * vop.w + 0.5)
292-
local h = math.floor((y2 - y1) * vop.h + 0.5)
293302
if active_mode == "hard" then
294-
local video_crop = tostring(w) .."x".. tostring(h) .."+".. tostring(x) .."+".. tostring(y)
295-
mp.set_property_native("video-crop", video_crop)
303+
local w = x2 - x1
304+
local h = y2 - y1
305+
306+
table.insert(recursive_crop, {x = x1, y = y1, w = w, h = h})
307+
apply_video_crop()
308+
table.insert(remove_last_filter, "hard")
309+
296310
elseif active_mode == "delogo" then
297311
local vf_table = mp.get_property_native("vf")
312+
313+
local x, y, w, h = adjust_coordinates()
314+
315+
local x = math.floor((x + x1 * w) * vop.w + 0.5)
316+
local y = math.floor((y + y1 * h) * vop.h + 0.5)
317+
local w = math.floor(w * (x2 - x1) * vop.w + 0.5)
318+
local h = math.floor(h * (y2 - y1) * vop.h + 0.5)
319+
298320
-- delogo is a little special and needs some padding to function
299321
w = math.min(vop.w - 1, w)
300322
h = math.min(vop.h - 1, h)
301323
x = math.max(1, x)
302324
y = math.max(1, y)
325+
303326
if x + w == vop.w then w = w - 1 end
304327
if y + h == vop.h then h = h - 1 end
328+
305329
vf_table[#vf_table + 1] = {
306330
name="delogo",
307331
params= { x = tostring(x), y = tostring(y), w = tostring(w), h = tostring(h) }
308332
}
333+
309334
mp.set_property_native("vf", vf_table)
335+
table.insert(remove_last_filter, "delogo")
310336
end
311337
end
312338
end
@@ -351,6 +377,136 @@ function cancel_crop()
351377
active = false
352378
end
353379

380+
-- adjust coordinates based on previous values
381+
function adjust_coordinates()
382+
local x, y, w, h = 0, 0, 1, 1
383+
for _, crop in ipairs(recursive_crop) do
384+
x = x + w * crop.x
385+
y = y + h * crop.y
386+
w = w * crop.w
387+
h = h * crop.h
388+
end
389+
return x, y, w, h
390+
end
391+
392+
function apply_video_crop()
393+
local x, y, w, h = adjust_coordinates()
394+
395+
local vop = mp.get_property_native("video-out-params")
396+
local x = math.floor(x * vop.w + 0.5)
397+
local y = math.floor(y * vop.h + 0.5)
398+
local w = math.floor(w * vop.w + 0.5)
399+
local h = math.floor(h * vop.h + 0.5)
400+
401+
local video_crop = tostring(w) .."x".. tostring(h) .."+".. tostring(x) .."+".. tostring(y)
402+
mp.set_property_native("video-crop", video_crop)
403+
end
404+
405+
function remove_filter(vf_table, filter_name, filter_number)
406+
local filter_count = 0
407+
local remove_last = 0
408+
for i = 1, #vf_table do
409+
if vf_table[i].name == filter_name then
410+
filter_count = filter_count + 1
411+
remove_last = i
412+
end
413+
end
414+
if filter_count > 0 then
415+
table.remove(vf_table, remove_last)
416+
mp.set_property_native("vf", vf_table)
417+
mp.osd_message("Removed: #" .. tostring(filter_number or filter_count) .. " " .. filter_name)
418+
return true
419+
end
420+
return false
421+
end
422+
423+
function remove_video_crop(filter_number)
424+
if #recursive_crop > 0 then
425+
table.remove(recursive_crop)
426+
-- reapply each crop in the table
427+
apply_video_crop()
428+
if #recursive_crop == 0 then
429+
mp.set_property_native("video-crop", "")
430+
end
431+
mp.osd_message("Removed: #" .. tostring(filter_number or #recursive_crop + 1) .. " " .. "video-crop")
432+
return true
433+
end
434+
return false
435+
end
436+
437+
function remove_zoom_pan(filter_number)
438+
if #recursive_zoom_pan > 0 then
439+
table.remove(recursive_zoom_pan)
440+
if #recursive_zoom_pan > 0 then
441+
local lastZoomPan = recursive_zoom_pan[#recursive_zoom_pan]
442+
mp.set_property("video-zoom", lastZoomPan.zoom)
443+
mp.set_property("video-pan-x", lastZoomPan.panX)
444+
mp.set_property("video-pan-y", lastZoomPan.panY)
445+
else
446+
mp.set_property("video-zoom", 0)
447+
mp.set_property("video-pan-x", 0)
448+
mp.set_property("video-pan-y", 0)
449+
end
450+
mp.osd_message("Removed: #" .. tostring(filter_number or #recursive_zoom_pan + 1) .. " " .. "soft-crop")
451+
return true
452+
end
453+
return false
454+
end
455+
456+
-- 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
457+
function remove_last_filter_entry(filter_type)
458+
for i = #remove_last_filter, 1, -1 do
459+
if remove_last_filter[i] == filter_type then
460+
table.remove(remove_last_filter, i)
461+
break
462+
end
463+
end
464+
end
465+
466+
function remove_crop(mode, order)
467+
local vf_table = mp.get_property_native("vf")
468+
local total_filters = #remove_last_filter
469+
470+
-- 'remove-crop all order' removes all filters starting with most recently added
471+
if order == "order" then
472+
if total_filters == 0 then
473+
mp.osd_message("Nothing to remove")
474+
return
475+
end
476+
local last_filter = table.remove(remove_last_filter)
477+
if last_filter == "hard" then
478+
remove_video_crop(total_filters)
479+
elseif last_filter == "delogo" then
480+
remove_filter(vf_table, "delogo", total_filters)
481+
elseif last_filter == "soft" then
482+
remove_zoom_pan(total_filters)
483+
end
484+
else
485+
local modes = {"delogo", "hard", "soft"}
486+
if order == "hard" then
487+
modes = {"hard", "soft", "delogo"}
488+
elseif order == "soft" then
489+
modes = {"soft", "hard", "delogo"}
490+
end
491+
492+
for _, mode_name in ipairs(modes) do
493+
if not mode or mode == "all" or mode == mode_name then
494+
if mode_name == "delogo" and remove_filter(vf_table, "delogo") then
495+
remove_last_filter_entry("delogo")
496+
return
497+
elseif mode_name == "hard" and remove_video_crop() then
498+
remove_last_filter_entry("hard")
499+
return
500+
elseif mode_name == "soft" and remove_zoom_pan() then
501+
remove_last_filter_entry("soft")
502+
return
503+
end
504+
end
505+
end
506+
mp.osd_message("Nothing to remove")
507+
end
508+
end
509+
354510
function start_crop(mode)
355511
if active then return end
356512
if not mp.get_property_native("osd-dimensions") then return end
@@ -388,37 +544,24 @@ function toggle_crop(mode)
388544
msg.error("Invalid mode value: " .. mode)
389545
end
390546
local toggle_mode = mode or opts.mode
391-
if toggle_mode == "soft" then return end -- can't toggle soft mode
392-
393-
local remove_delogo = function()
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 == "delogo" 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
408-
end
409-
if toggle_mode == "delogo" and not remove_delogo() then
547+
548+
if toggle_mode == "soft" and not remove_zoom_pan() then
410549
start_crop(mode)
550+
elseif toggle_mode == "soft" then
551+
remove_last_filter_entry("soft")
411552
end
412-
local remove_hard = function()
413-
video_crop = mp.get_property_native("video-crop")
414-
if video_crop == "" then
415-
return false
416-
end
417-
mp.set_property_native("video-crop", "")
418-
return true
553+
554+
local vf_table = mp.get_property_native("vf")
555+
if toggle_mode == "delogo" and not remove_filter(vf_table, "delogo") then
556+
start_crop(mode)
557+
elseif toggle_mode == "delogo" then
558+
remove_last_filter_entry("delogo")
419559
end
420-
if toggle_mode == "hard" and not remove_hard() then
560+
561+
if toggle_mode == "hard" and not remove_video_crop() then
421562
start_crop(mode)
563+
elseif toggle_mode == "hard" then
564+
remove_last_filter_entry("hard")
422565
end
423566
end
424567

@@ -449,5 +592,6 @@ bindings_repeat[opts.up_fine] = movement_func(0, -opts.fine_movement)
449592
bindings_repeat[opts.down_fine] = movement_func(0, opts.fine_movement)
450593

451594

595+
mp.add_key_binding(nil, "remove-crop", remove_crop)
452596
mp.add_key_binding(nil, "start-crop", start_crop)
453597
mp.add_key_binding(nil, "toggle-crop", toggle_crop)

0 commit comments

Comments
 (0)