dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

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)