home-config/mpv/scripts/easycrop.lua
7f27d13d617b245745e78fc2e79266e99f6d4c1b
· 6.7 KB · 254 lines
raw
| 1 | local msg = require('mp.msg') |
| 2 | local assdraw = require('mp.assdraw') |
| 3 | |
| 4 | local script_name = "easycrop" |
| 5 | |
| 6 | -- Number of crop points currently chosen (0 to 2) |
| 7 | local points = {} |
| 8 | -- True if in cropping selection mode |
| 9 | local cropping = false |
| 10 | -- Original value of osc property |
| 11 | local osc_prop = false |
| 12 | |
| 13 | -- Helper that converts two points to top-left and bottom-right |
| 14 | local swizzle_points = function (p1, p2) |
| 15 | if p1.x > p2.x then p1.x, p2.x = p2.x, p1.x end |
| 16 | if p1.y > p2.y then p1.y, p2.y = p2.y, p1.y end |
| 17 | end |
| 18 | |
| 19 | local clamp = function (val, min, max) |
| 20 | assert(min <= max) |
| 21 | if val < min then return min end |
| 22 | if val > max then return max end |
| 23 | return val |
| 24 | end |
| 25 | |
| 26 | local video_space_from_screen_space = function (ssp) |
| 27 | -- Video native dimensions and screen size |
| 28 | local vid_w = mp.get_property("width") |
| 29 | local vid_h = mp.get_property("height") |
| 30 | local osd_w = mp.get_property("osd-width") |
| 31 | local osd_h = mp.get_property("osd-height") |
| 32 | |
| 33 | -- Factor by which the video is scaled to fit the screen |
| 34 | local scale = math.min(osd_w/vid_w, osd_h/vid_h) |
| 35 | |
| 36 | -- Size video takes up in screen |
| 37 | local vid_sw, vid_sh = scale*vid_w, scale*vid_h |
| 38 | |
| 39 | -- Video offset within screen |
| 40 | local off_x = math.floor((osd_w - vid_sw)/2) |
| 41 | local off_y = math.floor((osd_h - vid_sh)/2) |
| 42 | |
| 43 | local vsp = {} |
| 44 | |
| 45 | -- Move the point to within the video |
| 46 | vsp.x = clamp(ssp.x, off_x, off_x + vid_sw) |
| 47 | vsp.y = clamp(ssp.y, off_y, off_y + vid_sh) |
| 48 | |
| 49 | -- Convert screen-space to video-space |
| 50 | vsp.x = math.floor((vsp.x - off_x) / scale) |
| 51 | vsp.y = math.floor((vsp.y - off_y) / scale) |
| 52 | |
| 53 | return vsp |
| 54 | end |
| 55 | |
| 56 | local screen_space_from_video_space = function (vsp) |
| 57 | -- Video native dimensions and screen size |
| 58 | local vid_w = mp.get_property("width") |
| 59 | local vid_h = mp.get_property("height") |
| 60 | local osd_w = mp.get_property("osd-width") |
| 61 | local osd_h = mp.get_property("osd-height") |
| 62 | |
| 63 | -- Factor by which the video is scaled to fit the screen |
| 64 | local scale = math.min(osd_w/vid_w, osd_h/vid_h) |
| 65 | |
| 66 | -- Size video takes up in screen |
| 67 | local vid_sw, vid_sh = scale*vid_w, scale*vid_h |
| 68 | |
| 69 | -- Video offset within screen |
| 70 | local off_x = math.floor((osd_w - vid_sw)/2) |
| 71 | local off_y = math.floor((osd_h - vid_sh)/2) |
| 72 | |
| 73 | local ssp = {} |
| 74 | ssp.x = vsp.x * scale + off_x |
| 75 | ssp.y = vsp.y * scale + off_y |
| 76 | return ssp |
| 77 | end |
| 78 | |
| 79 | -- Wrapper that converts RRGGBB / RRGGBBAA to ASS format |
| 80 | local ass_set_color = function (idx, color) |
| 81 | assert(color:len() == 8 or color:len() == 6) |
| 82 | local ass = "" |
| 83 | |
| 84 | -- Set alpha value (if present) |
| 85 | if color:len() == 8 then |
| 86 | local alpha = 0xff - tonumber(color:sub(7, 8), 16) |
| 87 | ass = ass .. string.format("\\%da&H%X&", idx, alpha) |
| 88 | end |
| 89 | |
| 90 | -- Swizzle RGB to BGR and build ASS string |
| 91 | color = color:sub(5, 6) .. color:sub(3, 4) .. color:sub(1, 2) |
| 92 | return "{" .. ass .. string.format("\\%dc&H%s&", idx, color) .. "}" |
| 93 | end |
| 94 | |
| 95 | local draw_rect = function (p1, p2) |
| 96 | local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height") |
| 97 | |
| 98 | ass = assdraw.ass_new() |
| 99 | |
| 100 | -- Draw overlay over surrounding unselected region |
| 101 | |
| 102 | ass:draw_start() |
| 103 | ass:pos(0, 0) |
| 104 | |
| 105 | ass:append(ass_set_color(1, "000000aa")) |
| 106 | ass:append(ass_set_color(3, "00000000")) |
| 107 | |
| 108 | local l = math.min(p1.x, p2.x) |
| 109 | local r = math.max(p1.x, p2.x) |
| 110 | local u = math.min(p1.y, p2.y) |
| 111 | local d = math.max(p1.y, p2.y) |
| 112 | |
| 113 | ass:rect_cw(0, 0, l, osd_h) |
| 114 | ass:rect_cw(r, 0, osd_w, osd_h) |
| 115 | ass:rect_cw(l, 0, r, u) |
| 116 | ass:rect_cw(l, d, r, osd_h) |
| 117 | |
| 118 | ass:draw_stop() |
| 119 | |
| 120 | -- Draw border around selected region |
| 121 | |
| 122 | ass:new_event() |
| 123 | ass:draw_start() |
| 124 | ass:pos(0, 0) |
| 125 | |
| 126 | ass:append(ass_set_color(1, "00000000")) |
| 127 | ass:append(ass_set_color(3, "000000ff")) |
| 128 | ass:append("{\\bord2}") |
| 129 | |
| 130 | ass:rect_cw(p1.x, p1.y, p2.x, p2.y) |
| 131 | |
| 132 | ass:draw_stop() |
| 133 | |
| 134 | mp.set_osd_ass(osd_w, osd_h, ass.text) |
| 135 | end |
| 136 | |
| 137 | local draw_fill = function () |
| 138 | local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height") |
| 139 | |
| 140 | ass = assdraw.ass_new() |
| 141 | ass:draw_start() |
| 142 | ass:pos(0, 0) |
| 143 | |
| 144 | ass:append(ass_set_color(1, "000000aa")) |
| 145 | ass:append(ass_set_color(3, "00000000")) |
| 146 | ass:rect_cw(0, 0, osd_w, osd_h) |
| 147 | |
| 148 | ass:draw_stop() |
| 149 | mp.set_osd_ass(osd_w, osd_h, ass.text) |
| 150 | end |
| 151 | |
| 152 | local draw_clear = function () |
| 153 | local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height") |
| 154 | mp.set_osd_ass(osd_w, osd_h, "") |
| 155 | end |
| 156 | |
| 157 | local draw_cropper = function () |
| 158 | if #points == 1 then |
| 159 | local p1 = screen_space_from_video_space(points[1]) |
| 160 | local p2 = {} |
| 161 | p2.x, p2.y = mp.get_mouse_pos() |
| 162 | draw_rect(p1, p2) |
| 163 | end |
| 164 | end |
| 165 | |
| 166 | local uncrop = function () |
| 167 | mp.command("no-osd vf del @" .. script_name .. ":crop") |
| 168 | end |
| 169 | |
| 170 | local crop = function(p1, p2) |
| 171 | swizzle_points(p1, p2) |
| 172 | |
| 173 | local w = p2.x - p1.x |
| 174 | local h = p2.y - p1.y |
| 175 | local ok, err = mp.command(string.format( |
| 176 | "no-osd vf add @%s:crop=%s:%s:%s:%s", script_name, w, h, p1.x, p1.y)) |
| 177 | |
| 178 | if not ok then |
| 179 | mp.osd_message("Cropping failed") |
| 180 | points = {} |
| 181 | end |
| 182 | end |
| 183 | |
| 184 | local easycrop_stop = function () |
| 185 | mp.set_property("osc", osc_prop) |
| 186 | cropping = false |
| 187 | mp.remove_key_binding("easycrop_mouse_btn0") |
| 188 | draw_clear() |
| 189 | end |
| 190 | |
| 191 | local mouse_btn0_cb = function () |
| 192 | if not cropping then |
| 193 | return |
| 194 | end |
| 195 | |
| 196 | local mx, my = mp.get_mouse_pos() |
| 197 | table.insert(points, video_space_from_screen_space({ x = mx, y = my })) |
| 198 | |
| 199 | if #points == 2 then |
| 200 | crop(points[1], points[2]) |
| 201 | easycrop_stop() |
| 202 | end |
| 203 | end |
| 204 | |
| 205 | local easycrop_start = function () |
| 206 | -- Cropping requires swdec or hwdec with copy-back |
| 207 | local hwdec = mp.get_property("hwdec-current") |
| 208 | if hwdec == nil then |
| 209 | return mp.msg.error("Cannot determine current hardware decoder mode") |
| 210 | end |
| 211 | -- Check whitelist of ok values |
| 212 | local valid_hwdec = { |
| 213 | ["no"] = true, -- software decoding |
| 214 | -- Taken from mpv manual |
| 215 | ["videotoolbox-co"] = true, |
| 216 | ["vaapi-copy"] = true, |
| 217 | ["dxva2-copy"] = true, |
| 218 | ["d3d11va-copy"] = true, |
| 219 | ["mediacodec"] = true |
| 220 | } |
| 221 | if not valid_hwdec[hwdec] then |
| 222 | return mp.osd_message("Cropping requires swdec or hwdec with copy-back (see mpv manual)") |
| 223 | end |
| 224 | |
| 225 | -- Just clear the current crop and return, if there is one |
| 226 | if #points ~= 0 then |
| 227 | uncrop() |
| 228 | points = {} |
| 229 | return |
| 230 | end |
| 231 | |
| 232 | -- Hide OSC |
| 233 | osc_prop = mp.get_property("osc") |
| 234 | mp.set_property("osc", "no") |
| 235 | |
| 236 | cropping = true |
| 237 | mp.add_forced_key_binding("mouse_btn0", "easycrop_mouse_btn0", mouse_btn0_cb) |
| 238 | draw_fill() |
| 239 | end |
| 240 | |
| 241 | local easycrop_activate = function () |
| 242 | if cropping then |
| 243 | easycrop_stop() |
| 244 | else |
| 245 | easycrop_start() |
| 246 | end |
| 247 | end |
| 248 | |
| 249 | mp.add_key_binding("mouse_move", draw_cropper) |
| 250 | mp.observe_property("osd-width", "native", draw_cropper) |
| 251 | mp.observe_property("osd-height", "native", draw_cropper) |
| 252 | |
| 253 | mp.add_key_binding("c", "easy_crop", easycrop_activate) |
| 254 |