crop.lua (13720B)
1 local opts = { 2 draw_shade = true, 3 shade_opacity = "77", 4 draw_crosshair = true, 5 draw_text = true, 6 mouse_support=true, 7 coarse_movement=30, 8 left_coarse="LEFT", 9 right_coarse="RIGHT", 10 up_coarse="UP", 11 down_coarse="DOWN", 12 fine_movement=1, 13 left_fine="ALT+LEFT", 14 right_fine="ALT+RIGHT", 15 up_fine="ALT+UP", 16 down_fine="ALT+DOWN", 17 accept="ENTER,MOUSE_BTN0", 18 cancel="ESC", 19 } 20 (require 'mp.options').read_options(opts) 21 22 function split(input) 23 local ret = {} 24 for str in string.gmatch(input, "([^,]+)") do 25 ret[#ret + 1] = str 26 end 27 return ret 28 end 29 opts.accept = split(opts.accept) 30 opts.cancel = split(opts.cancel) 31 32 local assdraw = require 'mp.assdraw' 33 local msg = require 'mp.msg' 34 local needs_drawing = false 35 local dimensions_changed = false 36 local crop_first_corner = nil -- in video space 37 local crop_cursor = { 38 x = -1, 39 y = -1 40 } 41 42 function get_video_dimensions() 43 if not dimensions_changed then return _video_dimensions end 44 -- this function is very much ripped from video/out/aspect.c in mpv's source 45 local video_params = mp.get_property_native("video-out-params") 46 if not video_params then return nil end 47 dimensions_changed = false 48 local keep_aspect = mp.get_property_bool("keepaspect") 49 local w = video_params["w"] 50 local h = video_params["h"] 51 local dw = video_params["dw"] 52 local dh = video_params["dh"] 53 if mp.get_property_number("video-rotate") % 180 == 90 then 54 w, h = h,w 55 dw, dh = dh, dw 56 end 57 _video_dimensions = { 58 top_left = {}, 59 bottom_right = {}, 60 ratios = {}, 61 } 62 if keep_aspect then 63 local unscaled = mp.get_property_native("video-unscaled") 64 local panscan = mp.get_property_number("panscan") 65 local window_w, window_h = mp.get_osd_size() 66 67 local fwidth = window_w 68 local fheight = math.floor(window_w / dw * dh) 69 if fheight > window_h or fheight < h then 70 local tmpw = math.floor(window_h / dh * dw) 71 if tmpw <= window_w then 72 fheight = window_h 73 fwidth = tmpw 74 end 75 end 76 local vo_panscan_area = window_h - fheight 77 local f_w = fwidth / fheight 78 local f_h = 1 79 if vo_panscan_area == 0 then 80 vo_panscan_area = window_h - fwidth 81 f_w = 1 82 f_h = fheight / fwidth 83 end 84 if unscaled or unscaled == "downscale-big" then 85 vo_panscan_area = 0 86 if unscaled or (dw <= window_w and dh <= window_h) then 87 fwidth = dw 88 fheight = dh 89 end 90 end 91 92 local scaled_width = fwidth + math.floor(vo_panscan_area * panscan * f_w) 93 local scaled_height = fheight + math.floor(vo_panscan_area * panscan * f_h) 94 95 local split_scaling = function (dst_size, scaled_src_size, zoom, align, pan) 96 scaled_src_size = math.floor(scaled_src_size * 2 ^ zoom) 97 align = (align + 1) / 2 98 local dst_start = math.floor((dst_size - scaled_src_size) * align + pan * scaled_src_size) 99 if dst_start < 0 then 100 --account for C int cast truncating as opposed to flooring 101 dst_start = dst_start + 1 102 end 103 local dst_end = dst_start + scaled_src_size; 104 if dst_start >= dst_end then 105 dst_start = 0 106 dst_end = 1 107 end 108 return dst_start, dst_end 109 end 110 local zoom = mp.get_property_number("video-zoom") 111 112 local align_x = mp.get_property_number("video-align-x") 113 local pan_x = mp.get_property_number("video-pan-x") 114 _video_dimensions.top_left.x, _video_dimensions.bottom_right.x = split_scaling(window_w, scaled_width, zoom, align_x, pan_x) 115 116 local align_y = mp.get_property_number("video-align-y") 117 local pan_y = mp.get_property_number("video-pan-y") 118 _video_dimensions.top_left.y, _video_dimensions.bottom_right.y = split_scaling(window_h, scaled_height, zoom, align_y, pan_y) 119 else 120 _video_dimensions.top_left.x = 0 121 _video_dimensions.bottom_right.x = window_w 122 _video_dimensions.top_left.y = 0 123 _video_dimensions.bottom_right.y = window_h 124 end 125 _video_dimensions.ratios.w = w / (_video_dimensions.bottom_right.x - _video_dimensions.top_left.x) 126 _video_dimensions.ratios.h = h / (_video_dimensions.bottom_right.y - _video_dimensions.top_left.y) 127 return _video_dimensions 128 end 129 130 function sort_corners(c1, c2) 131 local r1, r2 = {}, {} 132 if c1.x < c2.x then r1.x, r2.x = c1.x, c2.x else r1.x, r2.x = c2.x, c1.x end 133 if c1.y < c2.y then r1.y, r2.y = c1.y, c2.y else r1.y, r2.y = c2.y, c1.y end 134 return r1, r2 135 end 136 137 function clamp(low, value, high) 138 if value <= low then 139 return low 140 elseif value >= high then 141 return high 142 else 143 return value 144 end 145 end 146 147 function clamp_point(top_left, point, bottom_right) 148 return { 149 x = clamp(top_left.x, point.x, bottom_right.x), 150 y = clamp(top_left.y, point.y, bottom_right.y) 151 } 152 end 153 154 function screen_to_video(point, video_dim) 155 return { 156 x = math.floor(video_dim.ratios.w * (point.x - video_dim.top_left.x) + 0.5), 157 y = math.floor(video_dim.ratios.h * (point.y - video_dim.top_left.y) + 0.5) 158 } 159 end 160 161 function video_to_screen(point, video_dim) 162 return { 163 x = math.floor(point.x / video_dim.ratios.w + video_dim.top_left.x + 0.5), 164 y = math.floor(point.y / video_dim.ratios.h + video_dim.top_left.y + 0.5) 165 } 166 end 167 168 function draw_shade(ass, unshaded, video) 169 ass:new_event() 170 ass:pos(0, 0) 171 ass:append("{\\bord0}") 172 ass:append("{\\shad0}") 173 ass:append("{\\c&H000000&}") 174 ass:append("{\\1a&H" .. opts.shade_opacity .. "}") 175 ass:append("{\\2a&HFF}") 176 ass:append("{\\3a&HFF}") 177 ass:append("{\\4a&HFF}") 178 local c1, c2 = unshaded.top_left, unshaded.bottom_right 179 local v = video 180 -- c1.x c2.x 181 -- +-----+------------+ 182 -- | | ur | 183 -- c1.y| ul +-------+----+ 184 -- | | | | 185 -- c2.y+-----+-------+ lr | 186 -- | ll | | 187 -- +-------------+----+ 188 ass:draw_start() 189 ass:rect_cw(v.top_left.x, v.top_left.y, c1.x, c2.y) -- ul 190 ass:rect_cw(c1.x, v.top_left.y, v.bottom_right.x, c1.y) -- ur 191 ass:rect_cw(v.top_left.x, c2.y, c2.x, v.bottom_right.y) -- ll 192 ass:rect_cw(c2.x, c1.y, v.bottom_right.x, v.bottom_right.y) -- lr 193 ass:draw_stop() 194 -- also possible to draw a rect over the whole video 195 -- and \iclip it in the middle, but seemingy slower 196 end 197 198 function draw_crosshair(ass, center, window_size) 199 ass:new_event() 200 ass:append("{\\bord0}") 201 ass:append("{\\shad0}") 202 ass:append("{\\c&HBBBBBB&}") 203 ass:append("{\\1a&H00&}") 204 ass:append("{\\2a&HFF&}") 205 ass:append("{\\3a&HFF&}") 206 ass:append("{\\4a&HFF&}") 207 ass:pos(0, 0) 208 ass:draw_start() 209 ass:rect_cw(center.x - 0.5, 0, center.x + 0.5, window_size.h) 210 ass:rect_cw(0, center.y - 0.5, window_size.w, center.y + 0.5) 211 ass:draw_stop() 212 end 213 214 function draw_position_text(ass, text, position, window_size, offset) 215 ass:new_event() 216 local align = 1 217 local ofx = 1 218 local ofy = -1 219 if position.x > window_size.w / 2 then 220 align = align + 2 221 ofx = -1 222 end 223 if position.y < window_size.h / 2 then 224 align = align + 6 225 ofy = 1 226 end 227 ass:append("{\\an"..align.."}") 228 ass:append("{\\fs26}") 229 ass:append("{\\bord1.5}") 230 ass:pos(ofx*offset + position.x, ofy*offset + position.y) 231 ass:append(text) 232 end 233 234 function draw_crop_zone() 235 if needs_drawing then 236 local video_dim = get_video_dimensions() 237 if not video_dim then 238 cancel_crop() 239 return 240 end 241 242 local window_size = {} 243 window_size.w, window_size.h = mp.get_osd_size() 244 crop_cursor = clamp_point(video_dim.top_left, crop_cursor, video_dim.bottom_right) 245 local ass = assdraw.ass_new() 246 247 if opts.draw_shade and crop_first_corner then 248 local first_corner = video_to_screen(crop_first_corner, video_dim) 249 local unshaded = {} 250 unshaded.top_left, unshaded.bottom_right = sort_corners(first_corner, crop_cursor) 251 -- don't draw shade over non-visible video parts 252 local window = { 253 top_left = { x = 0, y = 0 }, 254 bottom_right = { x = window_size.w, y = window_size.h }, 255 } 256 local video_visible = { 257 top_left = clamp_point(window.top_left, video_dim.top_left, window.bottom_right), 258 bottom_right = clamp_point(window.top_left, video_dim.bottom_right, window.bottom_right), 259 } 260 draw_shade(ass, unshaded, video_visible) 261 end 262 263 if opts.draw_crosshair then 264 draw_crosshair(ass, crop_cursor, window_size) 265 end 266 267 if opts.draw_text then 268 cursor_video = screen_to_video(crop_cursor, video_dim) 269 local text = string.format("%d, %d", cursor_video.x, cursor_video.y) 270 if crop_first_corner then 271 text = string.format("%s (%dx%d)", text, 272 math.abs(cursor_video.x - crop_first_corner.x), 273 math.abs(cursor_video.y - crop_first_corner.y) 274 ) 275 end 276 draw_position_text(ass, text, crop_cursor, window_size, 6) 277 end 278 279 mp.set_osd_ass(window_size.w, window_size.h, ass.text) 280 needs_drawing = false 281 end 282 end 283 284 function crop_video(x, y, w, h) 285 local vf_table = mp.get_property_native("vf") 286 vf_table[#vf_table + 1] = { 287 name="crop", 288 params= { 289 x = tostring(x), 290 y = tostring(y), 291 w = tostring(w), 292 h = tostring(h) 293 } 294 } 295 mp.set_property_native("vf", vf_table) 296 end 297 298 function update_crop_zone_state() 299 local dim = get_video_dimensions() 300 if not dim then 301 cancel_crop() 302 return 303 end 304 crop_cursor = clamp_point(dim.top_left, crop_cursor, dim.bottom_right) 305 corner_video = screen_to_video(crop_cursor, dim) 306 if crop_first_corner == nil then 307 crop_first_corner = corner_video 308 needs_drawing = true 309 else 310 local c1, c2 = sort_corners(crop_first_corner, corner_video) 311 crop_video(c1.x, c1.y, c2.x - c1.x, c2.y - c1.y) 312 cancel_crop() 313 end 314 end 315 316 function reset_crop() 317 dimensions_changed = true 318 needs_drawing = true 319 end 320 321 local bindings = {} 322 local bindings_repeat = {} 323 324 function cancel_crop() 325 needs_drawing = false 326 crop_first_corner = nil 327 for key, _ in pairs(bindings) do 328 mp.remove_key_binding("crop-"..key) 329 end 330 for key, _ in pairs(bindings_repeat) do 331 mp.remove_key_binding("crop-"..key) 332 end 333 mp.unobserve_property(reset_crop) 334 mp.unregister_idle(draw_crop_zone) 335 mp.set_osd_ass(1280, 720, '') 336 end 337 338 -- bindings 339 if opts.mouse_support then 340 bindings["MOUSE_MOVE"] = function() crop_cursor.x, crop_cursor.y = mp.get_mouse_pos(); needs_drawing = true end 341 end 342 for _, key in ipairs(opts.accept) do 343 bindings[key] = update_crop_zone_state 344 end 345 for _, key in ipairs(opts.cancel) do 346 bindings[key] = cancel_crop 347 end 348 function movement_func(move_x, move_y) 349 return function() 350 crop_cursor.x = crop_cursor.x + move_x 351 crop_cursor.y = crop_cursor.y + move_y 352 needs_drawing = true 353 end 354 end 355 bindings_repeat[opts.left_coarse] = movement_func(-opts.coarse_movement, 0) 356 bindings_repeat[opts.right_coarse] = movement_func(opts.coarse_movement, 0) 357 bindings_repeat[opts.up_coarse] = movement_func(0, -opts.coarse_movement) 358 bindings_repeat[opts.down_coarse] = movement_func(0, opts.coarse_movement) 359 bindings_repeat[opts.left_fine] = movement_func(-opts.fine_movement, 0) 360 bindings_repeat[opts.right_fine] = movement_func(opts.fine_movement, 0) 361 bindings_repeat[opts.up_fine] = movement_func(0, -opts.fine_movement) 362 bindings_repeat[opts.down_fine] = movement_func(0, opts.fine_movement) 363 364 local properties = { 365 "keepaspect", 366 "video-out-params", 367 "video-unscaled", 368 "panscan", 369 "video-zoom", 370 "video-align-x", 371 "video-pan-x", 372 "video-align-y", 373 "video-pan-y", 374 "osd-width", 375 "osd-height", 376 } 377 378 function start_crop() 379 if not mp.get_property("video-out-params", nil) then return end 380 local hwdec = mp.get_property("hwdec-current") 381 if hwdec and hwdec ~= "no" and not string.find(hwdec, "-copy$") then 382 msg.error("Cannot crop with hardware decoding active (see manual)") 383 return 384 end 385 386 crop_cursor.x, crop_cursor.y = mp.get_mouse_pos() 387 needs_drawing = true 388 dimensions_changed = true 389 for key, func in pairs(bindings) do 390 mp.add_forced_key_binding(key, "crop-"..key, func) 391 end 392 for key, func in pairs(bindings_repeat) do 393 mp.add_forced_key_binding(key, "crop-"..key, func, { repeatable = true }) 394 end 395 mp.register_idle(draw_crop_zone) 396 for _, p in ipairs(properties) do 397 mp.observe_property(p, "native", reset_crop) 398 end 399 end 400 401 function toggle_crop() 402 local vf_table = mp.get_property_native("vf") 403 if #vf_table > 0 then 404 for i = #vf_table, 1, -1 do 405 if vf_table[i].name == "crop" then 406 for j = i, #vf_table-1 do 407 vf_table[j] = vf_table[j+1] 408 end 409 vf_table[#vf_table] = nil 410 mp.set_property_native("vf", vf_table) 411 return 412 end 413 end 414 end 415 start_crop() 416 end 417 418 mp.add_key_binding(nil, "start-crop", start_crop) 419 mp.add_key_binding(nil, "toggle-crop", toggle_crop)