summaryrefslogtreecommitdiff
path: root/ar/.config/mpv/scripts/mpv_crop_script.lua
diff options
context:
space:
mode:
Diffstat (limited to 'ar/.config/mpv/scripts/mpv_crop_script.lua')
-rw-r--r--ar/.config/mpv/scripts/mpv_crop_script.lua5471
1 files changed, 2868 insertions, 2603 deletions
diff --git a/ar/.config/mpv/scripts/mpv_crop_script.lua b/ar/.config/mpv/scripts/mpv_crop_script.lua
index c410fd3..7b3f7be 100644
--- a/ar/.config/mpv/scripts/mpv_crop_script.lua
+++ b/ar/.config/mpv/scripts/mpv_crop_script.lua
@@ -13,391 +13,425 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-]]--
+]]
+--
--[[
mpv_crop_script.lua 0.5.0 - commit 472281e (branch master)
Built on 2018-09-30 14:22:46
-]]--
+]]
+--
--[[
Assorted helper functions, from checking falsey values to path utils
to escaping and wrapping strings.
Does not depend on other libs.
-]]--
+]]
+--
-local assdraw = require 'mp.assdraw'
-local msg = require 'mp.msg'
-local utils = require 'mp.utils'
+local assdraw = require("mp.assdraw")
+local msg = require("mp.msg")
+local utils = require("mp.utils")
-- Determine platform --
-ON_WINDOWS = (package.config:sub(1,1) ~= '/')
+ON_WINDOWS = (package.config:sub(1, 1) ~= "/")
-- Some helper functions needed to parse the options --
-function isempty(v) return (v == false) or (v == nil) or (v == "") or (v == 0) or (type(v) == "table" and next(v) == nil) end
+function isempty(v)
+ return (v == false) or (v == nil) or (v == "") or (v == 0) or (type(v) == "table" and next(v) == nil)
+end
-function divmod (a, b)
- return math.floor(a / b), a % b
+function divmod(a, b)
+ return math.floor(a / b), a % b
end
-- Better modulo
-function bmod( i, N )
- return (i % N + N) % N
+function bmod(i, N)
+ return (i % N + N) % N
end
-
-- Path utils
local path_utils = {
- abspath = true,
- split = true,
- dirname = true,
- basename = true,
-
- isabs = true,
- normcase = true,
- splitdrive = true,
- join = true,
- normpath = true,
- relpath = true,
+ abspath = true,
+ split = true,
+ dirname = true,
+ basename = true,
+
+ isabs = true,
+ normcase = true,
+ splitdrive = true,
+ join = true,
+ normpath = true,
+ relpath = true,
}
-- Helpers
path_utils._split_parts = function(path, sep)
- local path_parts = {}
- for c in path:gmatch('[^' .. sep .. ']+') do table.insert(path_parts, c) end
- return path_parts
+ local path_parts = {}
+ for c in path:gmatch("[^" .. sep .. "]+") do
+ table.insert(path_parts, c)
+ end
+ return path_parts
end
-- Common functions
path_utils.abspath = function(path)
- if not path_utils.isabs(path) then
- local cwd = os.getenv("PWD") or utils.getcwd()
- path = path_utils.join(cwd, path)
- end
- return path_utils.normpath(path)
+ if not path_utils.isabs(path) then
+ local cwd = os.getenv("PWD") or utils.getcwd()
+ path = path_utils.join(cwd, path)
+ end
+ return path_utils.normpath(path)
end
path_utils.split = function(path)
- local drive, path = path_utils.splitdrive(path)
- -- Technically unix path could contain a \, but meh
- local first_index, last_index = path:find('^.*[/\\]')
-
- if last_index == nil then
- return drive .. '', path
- else
- local head = path:sub(0, last_index-1)
- local tail = path:sub(last_index+1)
- if head == '' then head = sep end
- return drive .. head, tail
- end
+ local drive, path = path_utils.splitdrive(path)
+ -- Technically unix path could contain a \, but meh
+ local first_index, last_index = path:find("^.*[/\\]")
+
+ if last_index == nil then
+ return drive .. "", path
+ else
+ local head = path:sub(0, last_index - 1)
+ local tail = path:sub(last_index + 1)
+ if head == "" then
+ head = sep
+ end
+ return drive .. head, tail
+ end
end
path_utils.dirname = function(path)
- local head, tail = path_utils.split(path)
- return head
+ local head, tail = path_utils.split(path)
+ return head
end
path_utils.basename = function(path)
- local head, tail = path_utils.split(path)
- return tail
+ local head, tail = path_utils.split(path)
+ return tail
end
path_utils.expanduser = function(path)
- -- Expands the following from the start of the path:
- -- ~ to HOME
- -- ~~ to mpv config directory (first result of mp.find_config_file('.'))
- -- ~~desktop to Windows desktop, otherwise HOME
- -- ~~temp to Windows temp or /tmp/
-
- local first_index, last_index = path:find('^.-[/\\]')
- local head = path
- local tail = ''
-
- local sep = ''
-
- if last_index then
- head = path:sub(0, last_index-1)
- tail = path:sub(last_index+1)
- sep = path:sub(last_index, last_index)
- end
-
- if head == "~~desktop" then
- head = ON_WINDOWS and path_utils.join(os.getenv('USERPROFILE'), 'Desktop') or os.getenv('HOME')
- elseif head == "~~temp" then
- head = ON_WINDOWS and os.getenv('TEMP') or (os.getenv('TMP') or '/tmp/')
- elseif head == "~~" then
- local mpv_config_dir = mp.find_config_file('.')
- if mpv_config_dir then
- head = path_utils.dirname(mpv_config_dir)
- else
- msg.warn('Could not find mpv config directory (using mp.find_config_file), using temp instead')
- head = ON_WINDOWS and os.getenv('TEMP') or (os.getenv('TMP') or '/tmp/')
- end
- elseif head == "~" then
- head = ON_WINDOWS and os.getenv('USERPROFILE') or os.getenv('HOME')
- end
-
- return path_utils.normpath(path_utils.join(head .. sep, tail))
+ -- Expands the following from the start of the path:
+ -- ~ to HOME
+ -- ~~ to mpv config directory (first result of mp.find_config_file('.'))
+ -- ~~desktop to Windows desktop, otherwise HOME
+ -- ~~temp to Windows temp or /tmp/
+
+ local first_index, last_index = path:find("^.-[/\\]")
+ local head = path
+ local tail = ""
+
+ local sep = ""
+
+ if last_index then
+ head = path:sub(0, last_index - 1)
+ tail = path:sub(last_index + 1)
+ sep = path:sub(last_index, last_index)
+ end
+
+ if head == "~~desktop" then
+ head = ON_WINDOWS and path_utils.join(os.getenv("USERPROFILE"), "Desktop") or os.getenv("HOME")
+ elseif head == "~~temp" then
+ head = ON_WINDOWS and os.getenv("TEMP") or (os.getenv("TMP") or "/tmp/")
+ elseif head == "~~" then
+ local mpv_config_dir = mp.find_config_file(".")
+ if mpv_config_dir then
+ head = path_utils.dirname(mpv_config_dir)
+ else
+ msg.warn("Could not find mpv config directory (using mp.find_config_file), using temp instead")
+ head = ON_WINDOWS and os.getenv("TEMP") or (os.getenv("TMP") or "/tmp/")
+ end
+ elseif head == "~" then
+ head = ON_WINDOWS and os.getenv("USERPROFILE") or os.getenv("HOME")
+ end
+
+ return path_utils.normpath(path_utils.join(head .. sep, tail))
end
-
if ON_WINDOWS then
- local sep = '\\'
- local altsep = '/'
- local curdir = '.'
- local pardir = '..'
- local colon = ':'
-
- local either_sep = function(c) return c == sep or c == altsep end
-
- path_utils.isabs = function(path)
- local prefix, path = path_utils.splitdrive(path)
- return either_sep(path:sub(1,1))
- end
-
- path_utils.normcase = function(path)
- return path:gsub(altsep, sep):lower()
- end
-
- path_utils.splitdrive = function(path)
- if #path >= 2 then
- local norm = path:gsub(altsep, sep)
- if (norm:sub(1, 2) == (sep..sep)) and (norm:sub(3,3) ~= sep) then
- -- UNC path
- local index = norm:find(sep, 3)
- if not index then
- return '', path
- end
-
- local index2 = norm:find(sep, index + 1)
- if index2 == index + 1 then
- return '', path
- elseif not index2 then
- index2 = path:len()
- end
-
- return path:sub(1, index2-1), path:sub(index2)
- elseif norm:sub(2,2) == colon then
- return path:sub(1, 2), path:sub(3)
- end
- end
- return '', path
- end
-
- path_utils.join = function(path, ...)
- local paths = {...}
-
- local result_drive, result_path = path_utils.splitdrive(path)
-
- function inner(p)
- local p_drive, p_path = path_utils.splitdrive(p)
- if either_sep(p_path:sub(1,1)) then
- -- Path is absolute
- if p_drive ~= '' or result_drive == '' then
- result_drive = p_drive
- end
- result_path = p_path
- return
- elseif p_drive ~= '' and p_drive ~= result_drive then
- if p_drive:lower() ~= result_drive:lower() then
- -- Different paths, ignore first
- result_drive = p_drive
- result_path = p_path
- return
- end
- end
-
- if result_path ~= '' and not either_sep(result_path:sub(-1)) then
- result_path = result_path .. sep
- end
- result_path = result_path .. p_path
- end
-
- for i, p in ipairs(paths) do inner(p) end
-
- -- add separator between UNC and non-absolute path
- if result_path ~= '' and not either_sep(result_path:sub(1,1)) and
- result_drive ~= '' and result_drive:sub(-1) ~= colon then
- return result_drive .. sep .. result_path
- end
- return result_drive .. result_path
- end
-
- path_utils.normpath = function(path)
- if path:find('\\\\.\\', nil, true) == 1 or path:find('\\\\?\\', nil, true) == 1 then
- -- Device names and literal paths - return as-is
- return path
- end
-
- path = path:gsub(altsep, sep)
- local prefix, path = path_utils.splitdrive(path)
-
- if path:find(sep) == 1 then
- prefix = prefix .. sep
- path = path:gsub('^[\\]+', '')
- end
-
- local comps = path_utils._split_parts(path, sep)
-
- local i = 1
- while i <= #comps do
- if comps[i] == curdir then
- table.remove(comps, i)
- elseif comps[i] == pardir then
- if i > 1 and comps[i-1] ~= pardir then
- table.remove(comps, i)
- table.remove(comps, i-1)
- i = i - 1
- elseif i == 1 and prefix:match('\\$') then
- table.remove(comps, i)
- else
- i = i + 1
- end
- else
- i = i + 1
- end
- end
-
- if prefix == '' and #comps == 0 then
- comps[1] = curdir
- end
-
- return prefix .. table.concat(comps, sep)
- end
-
- path_utils.relpath = function(path, start)
- start = start or curdir
-
- local start_abs = path_utils.abspath(path_utils.normpath(start))
- local path_abs = path_utils.abspath(path_utils.normpath(path))
-
- local start_drive, start_rest = path_utils.splitdrive(start_abs)
- local path_drive, path_rest = path_utils.splitdrive(path_abs)
-
- if path_utils.normcase(start_drive) ~= path_utils.normcase(path_drive) then
- -- Different drives
- return nil
- end
-
- local start_list = path_utils._split_parts(start_rest, sep)
- local path_list = path_utils._split_parts(path_rest, sep)
-
- local i = 1
- for j = 1, math.min(#start_list, #path_list) do
- if path_utils.normcase(start_list[j]) ~= path_utils.normcase(path_list[j]) then
- break
- end
- i = j + 1
- end
-
- local rel_list = {}
- for j = 1, (#start_list - i + 1) do rel_list[j] = pardir end
- for j = i, #path_list do table.insert(rel_list, path_list[j]) end
-
- if #rel_list == 0 then
- return curdir
- end
-
- return path_utils.join(unpack(rel_list))
- end
-
+ local sep = "\\"
+ local altsep = "/"
+ local curdir = "."
+ local pardir = ".."
+ local colon = ":"
+
+ local either_sep = function(c)
+ return c == sep or c == altsep
+ end
+
+ path_utils.isabs = function(path)
+ local prefix, path = path_utils.splitdrive(path)
+ return either_sep(path:sub(1, 1))
+ end
+
+ path_utils.normcase = function(path)
+ return path:gsub(altsep, sep):lower()
+ end
+
+ path_utils.splitdrive = function(path)
+ if #path >= 2 then
+ local norm = path:gsub(altsep, sep)
+ if (norm:sub(1, 2) == (sep .. sep)) and (norm:sub(3, 3) ~= sep) then
+ -- UNC path
+ local index = norm:find(sep, 3)
+ if not index then
+ return "", path
+ end
+
+ local index2 = norm:find(sep, index + 1)
+ if index2 == index + 1 then
+ return "", path
+ elseif not index2 then
+ index2 = path:len()
+ end
+
+ return path:sub(1, index2 - 1), path:sub(index2)
+ elseif norm:sub(2, 2) == colon then
+ return path:sub(1, 2), path:sub(3)
+ end
+ end
+ return "", path
+ end
+
+ path_utils.join = function(path, ...)
+ local paths = { ... }
+
+ local result_drive, result_path = path_utils.splitdrive(path)
+
+ function inner(p)
+ local p_drive, p_path = path_utils.splitdrive(p)
+ if either_sep(p_path:sub(1, 1)) then
+ -- Path is absolute
+ if p_drive ~= "" or result_drive == "" then
+ result_drive = p_drive
+ end
+ result_path = p_path
+ return
+ elseif p_drive ~= "" and p_drive ~= result_drive then
+ if p_drive:lower() ~= result_drive:lower() then
+ -- Different paths, ignore first
+ result_drive = p_drive
+ result_path = p_path
+ return
+ end
+ end
+
+ if result_path ~= "" and not either_sep(result_path:sub(-1)) then
+ result_path = result_path .. sep
+ end
+ result_path = result_path .. p_path
+ end
+
+ for i, p in ipairs(paths) do
+ inner(p)
+ end
+
+ -- add separator between UNC and non-absolute path
+ if
+ result_path ~= ""
+ and not either_sep(result_path:sub(1, 1))
+ and result_drive ~= ""
+ and result_drive:sub(-1) ~= colon
+ then
+ return result_drive .. sep .. result_path
+ end
+ return result_drive .. result_path
+ end
+
+ path_utils.normpath = function(path)
+ if path:find("\\\\.\\", nil, true) == 1 or path:find("\\\\?\\", nil, true) == 1 then
+ -- Device names and literal paths - return as-is
+ return path
+ end
+
+ path = path:gsub(altsep, sep)
+ local prefix, path = path_utils.splitdrive(path)
+
+ if path:find(sep) == 1 then
+ prefix = prefix .. sep
+ path = path:gsub("^[\\]+", "")
+ end
+
+ local comps = path_utils._split_parts(path, sep)
+
+ local i = 1
+ while i <= #comps do
+ if comps[i] == curdir then
+ table.remove(comps, i)
+ elseif comps[i] == pardir then
+ if i > 1 and comps[i - 1] ~= pardir then
+ table.remove(comps, i)
+ table.remove(comps, i - 1)
+ i = i - 1
+ elseif i == 1 and prefix:match("\\$") then
+ table.remove(comps, i)
+ else
+ i = i + 1
+ end
+ else
+ i = i + 1
+ end
+ end
+
+ if prefix == "" and #comps == 0 then
+ comps[1] = curdir
+ end
+
+ return prefix .. table.concat(comps, sep)
+ end
+
+ path_utils.relpath = function(path, start)
+ start = start or curdir
+
+ local start_abs = path_utils.abspath(path_utils.normpath(start))
+ local path_abs = path_utils.abspath(path_utils.normpath(path))
+
+ local start_drive, start_rest = path_utils.splitdrive(start_abs)
+ local path_drive, path_rest = path_utils.splitdrive(path_abs)
+
+ if path_utils.normcase(start_drive) ~= path_utils.normcase(path_drive) then
+ -- Different drives
+ return nil
+ end
+
+ local start_list = path_utils._split_parts(start_rest, sep)
+ local path_list = path_utils._split_parts(path_rest, sep)
+
+ local i = 1
+ for j = 1, math.min(#start_list, #path_list) do
+ if path_utils.normcase(start_list[j]) ~= path_utils.normcase(path_list[j]) then
+ break
+ end
+ i = j + 1
+ end
+
+ local rel_list = {}
+ for j = 1, (#start_list - i + 1) do
+ rel_list[j] = pardir
+ end
+ for j = i, #path_list do
+ table.insert(rel_list, path_list[j])
+ end
+
+ if #rel_list == 0 then
+ return curdir
+ end
+
+ return path_utils.join(unpack(rel_list))
+ end
else
- -- LINUX
- local sep = '/'
- local curdir = '.'
- local pardir = '..'
-
- path_utils.isabs = function(path) return path:sub(1,1) == '/' end
- path_utils.normcase = function(path) return path end
- path_utils.splitdrive = function(path) return '', path end
-
- path_utils.join = function(path, ...)
- local paths = {...}
-
- for i, p in ipairs(paths) do
- if p:sub(1,1) == sep then
- path = p
- elseif path == '' or path:sub(-1) == sep then
- path = path .. p
- else
- path = path .. sep .. p
- end
- end
-
- return path
- end
-
- path_utils.normpath = function(path)
- if path == '' then return curdir end
-
- local initial_slashes = (path:sub(1,1) == sep) and 1
- if initial_slashes and path:sub(2,2) == sep and path:sub(3,3) ~= sep then
- initial_slashes = 2
- end
-
- local comps = path_utils._split_parts(path, sep)
- local new_comps = {}
-
- for i, comp in ipairs(comps) do
- if comp == '' or comp == curdir then
- -- pass
- elseif (comp ~= pardir or (not initial_slashes and #new_comps == 0) or
- (#new_comps > 0 and new_comps[#new_comps] == pardir)) then
- table.insert(new_comps, comp)
- elseif #new_comps > 0 then
- table.remove(new_comps)
- end
- end
-
- comps = new_comps
- path = table.concat(comps, sep)
- if initial_slashes then
- path = sep:rep(initial_slashes) .. path
- end
-
- return (path ~= '') and path or curdir
- end
-
- path_utils.relpath = function(path, start)
- start = start or curdir
-
- local start_abs = path_utils.abspath(path_utils.normpath(start))
- local path_abs = path_utils.abspath(path_utils.normpath(path))
-
- local start_list = path_utils._split_parts(start_abs, sep)
- local path_list = path_utils._split_parts(path_abs, sep)
-
- local i = 1
- for j = 1, math.min(#start_list, #path_list) do
- if start_list[j] ~= path_list[j] then break
- end
- i = j + 1
- end
-
- local rel_list = {}
- for j = 1, (#start_list - i + 1) do rel_list[j] = pardir end
- for j = i, #path_list do table.insert(rel_list, path_list[j]) end
-
- if #rel_list == 0 then
- return curdir
- end
-
- return path_utils.join(unpack(rel_list))
- end
-
+ -- LINUX
+ local sep = "/"
+ local curdir = "."
+ local pardir = ".."
+
+ path_utils.isabs = function(path)
+ return path:sub(1, 1) == "/"
+ end
+ path_utils.normcase = function(path)
+ return path
+ end
+ path_utils.splitdrive = function(path)
+ return "", path
+ end
+
+ path_utils.join = function(path, ...)
+ local paths = { ... }
+
+ for i, p in ipairs(paths) do
+ if p:sub(1, 1) == sep then
+ path = p
+ elseif path == "" or path:sub(-1) == sep then
+ path = path .. p
+ else
+ path = path .. sep .. p
+ end
+ end
+
+ return path
+ end
+
+ path_utils.normpath = function(path)
+ if path == "" then
+ return curdir
+ end
+
+ local initial_slashes = (path:sub(1, 1) == sep) and 1
+ if initial_slashes and path:sub(2, 2) == sep and path:sub(3, 3) ~= sep then
+ initial_slashes = 2
+ end
+
+ local comps = path_utils._split_parts(path, sep)
+ local new_comps = {}
+
+ for i, comp in ipairs(comps) do
+ if comp == "" or comp == curdir then
+ -- pass
+ elseif
+ comp ~= pardir
+ or (not initial_slashes and #new_comps == 0)
+ or (#new_comps > 0 and new_comps[#new_comps] == pardir)
+ then
+ table.insert(new_comps, comp)
+ elseif #new_comps > 0 then
+ table.remove(new_comps)
+ end
+ end
+
+ comps = new_comps
+ path = table.concat(comps, sep)
+ if initial_slashes then
+ path = sep:rep(initial_slashes) .. path
+ end
+
+ return (path ~= "") and path or curdir
+ end
+
+ path_utils.relpath = function(path, start)
+ start = start or curdir
+
+ local start_abs = path_utils.abspath(path_utils.normpath(start))
+ local path_abs = path_utils.abspath(path_utils.normpath(path))
+
+ local start_list = path_utils._split_parts(start_abs, sep)
+ local path_list = path_utils._split_parts(path_abs, sep)
+
+ local i = 1
+ for j = 1, math.min(#start_list, #path_list) do
+ if start_list[j] ~= path_list[j] then
+ break
+ end
+ i = j + 1
+ end
+
+ local rel_list = {}
+ for j = 1, (#start_list - i + 1) do
+ rel_list[j] = pardir
+ end
+ for j = i, #path_list do
+ table.insert(rel_list, path_list[j])
+ end
+
+ if #rel_list == 0 then
+ return curdir
+ end
+
+ return path_utils.join(unpack(rel_list))
+ end
end
-- Path utils end
-- Check if path is local (by looking if it's prefixed by a proto://)
local path_is_local = function(path)
- local proto = path:match('(..-)://')
- return proto == nil
+ local proto = path:match("(..-)://")
+ return proto == nil
end
-
function Set(source)
- local set = {}
- for _, l in ipairs(source) do set[l] = true end
- return set
+ local set = {}
+ for _, l in ipairs(source) do
+ set[l] = true
+ end
+ return set
end
---------------------------
@@ -405,325 +439,327 @@ end
---------------------------
function busy_wait(seconds)
- local target = mp.get_time() + seconds
- local cycles = 0
- while target > mp.get_time() do
- cycles = cycles + 1
- end
- return cycles
+ local target = mp.get_time() + seconds
+ local cycles = 0
+ while target > mp.get_time() do
+ cycles = cycles + 1
+ end
+ return cycles
end
-- Removes all keys from a table, without destroying the reference to it
function clear_table(target)
- for key, value in pairs(target) do
- target[key] = nil
- end
+ for key, value in pairs(target) do
+ target[key] = nil
+ end
end
function shallow_copy(target)
- if type(target) == "table" then
- local copy = {}
- for k, v in pairs(target) do
- copy[k] = v
- end
- return copy
- else
- return target
- end
+ if type(target) == "table" then
+ local copy = {}
+ for k, v in pairs(target) do
+ copy[k] = v
+ end
+ return copy
+ else
+ return target
+ end
end
function deep_copy(target)
- local copy = {}
- for k, v in pairs(target) do
- if type(v) == "table" then
- copy[k] = deep_copy(v)
- else
- copy[k] = v
- end
- end
- return copy
+ local copy = {}
+ for k, v in pairs(target) do
+ if type(v) == "table" then
+ copy[k] = deep_copy(v)
+ else
+ copy[k] = v
+ end
+ end
+ return copy
end
-- Rounds to given decimals. eg. round_dec(3.145, 0) => 3
function round_dec(num, idp)
- local mult = 10^(idp or 0)
- return math.floor(num * mult + 0.5) / mult
+ local mult = 10 ^ (idp or 0)
+ return math.floor(num * mult + 0.5) / mult
end
function file_exists(name)
- local f = io.open(name, "rb")
- if f ~= nil then
- local ok, err, code = f:read(1)
- io.close(f)
- return code == nil
- else
- return false
- end
+ local f = io.open(name, "rb")
+ if f ~= nil then
+ local ok, err, code = f:read(1)
+ io.close(f)
+ return code == nil
+ else
+ return false
+ end
end
function path_exists(name)
- local f = io.open(name, "rb")
- if f ~= nil then
- io.close(f)
- return true
- else
- return false
- end
+ local f = io.open(name, "rb")
+ if f ~= nil then
+ io.close(f)
+ return true
+ else
+ return false
+ end
end
function create_directories(path)
- local cmd
- if ON_WINDOWS then
- cmd = { args = {'cmd', '/c', 'mkdir', path} }
- else
- cmd = { args = {'mkdir', '-p', path} }
- end
- utils.subprocess(cmd)
+ local cmd
+ if ON_WINDOWS then
+ cmd = { args = { "cmd", "/c", "mkdir", path } }
+ else
+ cmd = { args = { "mkdir", "-p", path } }
+ end
+ utils.subprocess(cmd)
end
function move_file(source_path, target_path)
- local cmd
- if ON_WINDOWS then
- cmd = { cancellable=false, args = {'cmd', '/c', 'move', '/Y', source_path, target_path } }
- utils.subprocess(cmd)
- else
- -- cmd = { cancellable=false, args = {'mv', source_path, target_path } }
- os.rename(source_path, target_path)
- end
+ local cmd
+ if ON_WINDOWS then
+ cmd = { cancellable = false, args = { "cmd", "/c", "move", "/Y", source_path, target_path } }
+ utils.subprocess(cmd)
+ else
+ -- cmd = { cancellable=false, args = {'mv', source_path, target_path } }
+ os.rename(source_path, target_path)
+ end
end
function check_pid(pid)
- -- Checks if a PID exists and returns true if so
- local cmd, r
- if ON_WINDOWS then
- cmd = { cancellable=false, args = {
- 'tasklist', '/FI', ('PID eq %d'):format(pid)
- }}
- r = utils.subprocess(cmd)
- return r.stdout:sub(1,1) == '\13'
- else
- cmd = { cancellable=false, args = {
- 'sh', '-c', ('kill -0 %d 2>/dev/null'):format(pid)
- }}
- r = utils.subprocess(cmd)
- return r.status == 0
- end
+ -- Checks if a PID exists and returns true if so
+ local cmd, r
+ if ON_WINDOWS then
+ cmd = { cancellable = false, args = {
+ "tasklist",
+ "/FI",
+ ("PID eq %d"):format(pid),
+ } }
+ r = utils.subprocess(cmd)
+ return r.stdout:sub(1, 1) == "\13"
+ else
+ cmd = { cancellable = false, args = {
+ "sh",
+ "-c",
+ ("kill -0 %d 2>/dev/null"):format(pid),
+ } }
+ r = utils.subprocess(cmd)
+ return r.status == 0
+ end
end
function kill_pid(pid)
- local cmd, r
- if ON_WINDOWS then
- cmd = { cancellable=false, args = {'taskkill', '/F', '/PID', tostring(pid) } }
- else
- cmd = { cancellable=false, args = {'kill', tostring(pid) } }
- end
- r = utils.subprocess(cmd)
- return r.status == 0, r
+ local cmd, r
+ if ON_WINDOWS then
+ cmd = { cancellable = false, args = { "taskkill", "/F", "/PID", tostring(pid) } }
+ else
+ cmd = { cancellable = false, args = { "kill", tostring(pid) } }
+ end
+ r = utils.subprocess(cmd)
+ return r.status == 0, r
end
-
-- Find an executable in PATH or CWD with the given name
function find_executable(name)
- local delim = ON_WINDOWS and ";" or ":"
+ local delim = ON_WINDOWS and ";" or ":"
- local pwd = os.getenv("PWD") or utils.getcwd()
- local path = os.getenv("PATH")
+ local pwd = os.getenv("PWD") or utils.getcwd()
+ local path = os.getenv("PATH")
- local env_path = pwd .. delim .. path -- Check CWD first
+ local env_path = pwd .. delim .. path -- Check CWD first
- local result, filename
- for path_dir in env_path:gmatch("[^"..delim.."]+") do
- filename = path_utils.join(path_dir, name)
- if file_exists(filename) then
- result = filename
- break
- end
- end
+ local result, filename
+ for path_dir in env_path:gmatch("[^" .. delim .. "]+") do
+ filename = path_utils.join(path_dir, name)
+ if file_exists(filename) then
+ result = filename
+ break
+ end
+ end
- return result
+ return result
end
local ExecutableFinder = { path_cache = {} }
-- Searches for an executable and caches the result if any
-function ExecutableFinder:get_executable_path( name, raw_name )
- name = ON_WINDOWS and not raw_name and (name .. ".exe") or name
+function ExecutableFinder:get_executable_path(name, raw_name)
+ name = ON_WINDOWS and not raw_name and (name .. ".exe") or name
- if self.path_cache[name] == nil then
- self.path_cache[name] = find_executable(name) or false
- end
- return self.path_cache[name]
+ if self.path_cache[name] == nil then
+ self.path_cache[name] = find_executable(name) or false
+ end
+ return self.path_cache[name]
end
-- Format seconds to HH.MM.SS.sss
function format_time(seconds, sep, decimals)
- decimals = decimals == nil and 3 or decimals
- sep = sep and sep or ":"
- local s = seconds
- local h, s = divmod(s, 60*60)
- local m, s = divmod(s, 60)
+ decimals = decimals == nil and 3 or decimals
+ sep = sep and sep or ":"
+ local s = seconds
+ local h, s = divmod(s, 60 * 60)
+ local m, s = divmod(s, 60)
- local second_format = string.format("%%0%d.%df", 2+(decimals > 0 and decimals+1 or 0), decimals)
+ local second_format = string.format("%%0%d.%df", 2 + (decimals > 0 and decimals + 1 or 0), decimals)
- return string.format("%02d"..sep.."%02d"..sep..second_format, h, m, s)
+ return string.format("%02d" .. sep .. "%02d" .. sep .. second_format, h, m, s)
end
-- Format seconds to 1h 2m 3.4s
function format_time_hms(seconds, sep, decimals, force_full)
- decimals = decimals == nil and 1 or decimals
- sep = sep ~= nil and sep or " "
+ decimals = decimals == nil and 1 or decimals
+ sep = sep ~= nil and sep or " "
- local s = seconds
- local h, s = divmod(s, 60*60)
- local m, s = divmod(s, 60)
+ local s = seconds
+ local h, s = divmod(s, 60 * 60)
+ local m, s = divmod(s, 60)
- if force_full or h > 0 then
- return string.format("%dh"..sep.."%dm"..sep.."%." .. tostring(decimals) .. "fs", h, m, s)
- elseif m > 0 then
- return string.format("%dm"..sep.."%." .. tostring(decimals) .. "fs", m, s)
- else
- return string.format("%." .. tostring(decimals) .. "fs", s)
- end
+ if force_full or h > 0 then
+ return string.format("%dh" .. sep .. "%dm" .. sep .. "%." .. tostring(decimals) .. "fs", h, m, s)
+ elseif m > 0 then
+ return string.format("%dm" .. sep .. "%." .. tostring(decimals) .. "fs", m, s)
+ else
+ return string.format("%." .. tostring(decimals) .. "fs", s)
+ end
end
-- Writes text on OSD and console
function log_info(txt, timeout)
- timeout = timeout or 1.5
- msg.info(txt)
- mp.osd_message(txt, timeout)
+ timeout = timeout or 1.5
+ msg.info(txt)
+ mp.osd_message(txt, timeout)
end
-- Join table items, ala ({"a", "b", "c"}, "=", "-", ", ") => "=a-, =b-, =c-"
function join_table(source, before, after, sep)
- before = before or ""
- after = after or ""
- sep = sep or ", "
- local result = ""
- for i, v in pairs(source) do
- if not isempty(v) then
- local part = before .. v .. after
- if i == 1 then
- result = part
- else
- result = result .. sep .. part
- end
- end
- end
- return result
+ before = before or ""
+ after = after or ""
+ sep = sep or ", "
+ local result = ""
+ for i, v in pairs(source) do
+ if not isempty(v) then
+ local part = before .. v .. after
+ if i == 1 then
+ result = part
+ else
+ result = result .. sep .. part
+ end
+ end
+ end
+ return result
end
function wrap(s, char)
- char = char or "'"
- return char .. s .. char
+ char = char or "'"
+ return char .. s .. char
end
-- Wraps given string into 'string' and escapes any 's in it
function escape_and_wrap(s, char, replacement)
- char = char or "'"
- replacement = replacement or "\\" .. char
- return wrap(string.gsub(s, char, replacement), char)
+ char = char or "'"
+ replacement = replacement or "\\" .. char
+ return wrap(string.gsub(s, char, replacement), char)
end
-- Escapes single quotes in a string and wraps the input in single quotes
function escape_single_bash(s)
- return escape_and_wrap(s, "'", "'\\''")
+ return escape_and_wrap(s, "'", "'\\''")
end
-- Returns (a .. b) if b is not empty or nil
function joined_or_nil(a, b)
- return not isempty(b) and (a .. b) or nil
+ return not isempty(b) and (a .. b) or nil
end
-- Put items from one table into another
function extend_table(target, source)
- for i, v in pairs(source) do
- table.insert(target, v)
- end
+ for i, v in pairs(source) do
+ table.insert(target, v)
+ end
end
-- Creates a handle and filename for a temporary random file (in current directory)
function create_temporary_file(base, mode, suffix)
- local handle, filename
- suffix = suffix or ""
- while true do
- filename = base .. tostring(math.random(1, 5000)) .. suffix
- handle = io.open(filename, "r")
- if not handle then
- handle = io.open(filename, mode)
- break
- end
- io.close(handle)
- end
- return handle, filename
+ local handle, filename
+ suffix = suffix or ""
+ while true do
+ filename = base .. tostring(math.random(1, 5000)) .. suffix
+ handle = io.open(filename, "r")
+ if not handle then
+ handle = io.open(filename, mode)
+ break
+ end
+ io.close(handle)
+ end
+ return handle, filename
end
-
function get_processor_count()
- local proc_count
-
- if ON_WINDOWS then
- proc_count = tonumber(os.getenv("NUMBER_OF_PROCESSORS"))
- else
- local cpuinfo_handle = io.open("/proc/cpuinfo")
- if cpuinfo_handle ~= nil then
- local cpuinfo_contents = cpuinfo_handle:read("*a")
- local _, replace_count = cpuinfo_contents:gsub('processor', '')
- proc_count = replace_count
- end
- end
-
- if proc_count and proc_count > 0 then
- return proc_count
- else
- return nil
- end
+ local proc_count
+
+ if ON_WINDOWS then
+ proc_count = tonumber(os.getenv("NUMBER_OF_PROCESSORS"))
+ else
+ local cpuinfo_handle = io.open("/proc/cpuinfo")
+ if cpuinfo_handle ~= nil then
+ local cpuinfo_contents = cpuinfo_handle:read("*a")
+ local _, replace_count = cpuinfo_contents:gsub("processor", "")
+ proc_count = replace_count
+ end
+ end
+
+ if proc_count and proc_count > 0 then
+ return proc_count
+ else
+ return nil
+ end
end
function substitute_values(string, values)
- local substitutor = function(match)
- if match == "%" then
- return "%"
- else
- -- nil is discarded by gsub
- return values[match]
- end
- end
+ local substitutor = function(match)
+ if match == "%" then
+ return "%"
+ else
+ -- nil is discarded by gsub
+ return values[match]
+ end
+ end
- local substituted = string:gsub('%%(.)', substitutor)
- return substituted
+ local substituted = string:gsub("%%(.)", substitutor)
+ return substituted
end
-- ASS HELPERS --
-function round_rect_top( ass, x0, y0, x1, y1, r )
- local c = 0.551915024494 * r -- circle approximation
- ass:move_to(x0 + r, y0)
- ass:line_to(x1 - r, y0) -- top line
- if r > 0 then
- ass:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r) -- top right corner
- end
- ass:line_to(x1, y1) -- right line
- ass:line_to(x0, y1) -- bottom line
- ass:line_to(x0, y0 + r) -- left line
- if r > 0 then
- ass:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0) -- top left corner
- end
+function round_rect_top(ass, x0, y0, x1, y1, r)
+ local c = 0.551915024494 * r -- circle approximation
+ ass:move_to(x0 + r, y0)
+ ass:line_to(x1 - r, y0) -- top line
+ if r > 0 then
+ ass:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r) -- top right corner
+ end
+ ass:line_to(x1, y1) -- right line
+ ass:line_to(x0, y1) -- bottom line
+ ass:line_to(x0, y0 + r) -- left line
+ if r > 0 then
+ ass:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0) -- top left corner
+ end
end
function round_rect(ass, x0, y0, x1, y1, rtl, rtr, rbr, rbl)
- local c = 0.551915024494
- ass:move_to(x0 + rtl, y0)
- ass:line_to(x1 - rtr, y0) -- top line
- if rtr > 0 then
- ass:bezier_curve(x1 - rtr + rtr*c, y0, x1, y0 + rtr - rtr*c, x1, y0 + rtr) -- top right corner
- end
- ass:line_to(x1, y1 - rbr) -- right line
- if rbr > 0 then
- ass:bezier_curve(x1, y1 - rbr + rbr*c, x1 - rbr + rbr*c, y1, x1 - rbr, y1) -- bottom right corner
- end
- ass:line_to(x0 + rbl, y1) -- bottom line
- if rbl > 0 then
- ass:bezier_curve(x0 + rbl - rbl*c, y1, x0, y1 - rbl + rbl*c, x0, y1 - rbl) -- bottom left corner
- end
- ass:line_to(x0, y0 + rtl) -- left line
- if rtl > 0 then
- ass:bezier_curve(x0, y0 + rtl - rtl*c, x0 + rtl - rtl*c, y0, x0 + rtl, y0) -- top left corner
- end
+ local c = 0.551915024494
+ ass:move_to(x0 + rtl, y0)
+ ass:line_to(x1 - rtr, y0) -- top line
+ if rtr > 0 then
+ ass:bezier_curve(x1 - rtr + rtr * c, y0, x1, y0 + rtr - rtr * c, x1, y0 + rtr) -- top right corner
+ end
+ ass:line_to(x1, y1 - rbr) -- right line
+ if rbr > 0 then
+ ass:bezier_curve(x1, y1 - rbr + rbr * c, x1 - rbr + rbr * c, y1, x1 - rbr, y1) -- bottom right corner
+ end
+ ass:line_to(x0 + rbl, y1) -- bottom line
+ if rbl > 0 then
+ ass:bezier_curve(x0 + rbl - rbl * c, y1, x0, y1 - rbl + rbl * c, x0, y1 - rbl) -- bottom left corner
+ end
+ ass:line_to(x0, y0 + rtl) -- left line
+ if rtl > 0 then
+ ass:bezier_curve(x0, y0 + rtl - rtl * c, x0 + rtl - rtl * c, y0, x0 + rtl, y0) -- top left corner
+ end
end
--[[
A slightly more advanced option parser for scripts.
@@ -732,492 +768,509 @@ end
original lines and appending changes to the end, along with profiles.
Does not depend on other libs.
-]]--
+]]
+--
local OptionParser = {}
OptionParser.__index = OptionParser
setmetatable(OptionParser, {
- __call = function (cls, ...) return cls.new(...) end
+ __call = function(cls, ...)
+ return cls.new(...)
+ end,
})
function OptionParser.new(identifier)
- local self = setmetatable({}, OptionParser)
-
- self.identifier = identifier
- self.config_file = self:_get_config_file(identifier)
-
- self.OVERRIDE_START = "# Script-saved overrides below this line. Edits will be lost!"
-
- -- All the options contained, as a list
- self.options_list = {}
- -- All the options contained, as a table with keys. See add_option
- self.options = {}
-
- self.default_profile = {name = "default", values = {}, loaded={}, config_lines = {}}
- self.profiles = {}
-
- self.active_profile = self.default_profile
-
- -- Recusing metatable magic to wrap self.values.key.sub_key into
- -- self.options["key.sub_key"].value, with support for assignments as well
- function get_value_or_mapper(key)
- local cur_option = self.options[key]
-
- if cur_option then
- -- Wrap tables
- if cur_option.type == "table" then
- return setmetatable({}, {
- __index = function(t, sub_key)
- return get_value_or_mapper(key .. "." .. sub_key)
- end,
- __newindex = function(t, sub_key, value)
- local sub_option = self.options[key .. "." .. sub_key]
- if sub_option and sub_option.type ~= "table" then
- self.active_profile.values[key .. "." .. sub_key] = value
- end
- end
- })
- else
- return self.active_profile.values[key]
- end
- end
- end
-
- -- Same recusing metatable magic to get the .default
- function get_default_or_mapper(key)
- local cur_option = self.options[key]
-
- if cur_option then
- if cur_option.type == "table" then
- return setmetatable({}, {
- __index = function(t, sub_key)
- return get_default_or_mapper(key .. "." .. sub_key)
- end,
- })
- else
- return cur_option.default
- -- return self.active_profile.values[key]
- end
- end
- end
-
- -- Easy lookups for values and defaults
- self.values = setmetatable({}, {
- __index = function(t, key)
- return get_value_or_mapper(key)
- end,
- __newindex = function(t, key, value)
- local option = self.options[key]
- if option then
- -- option.value = value
- self.active_profile.values[key] = value
- end
- end
- })
-
- self.defaults = setmetatable({}, {
- __index = function(t, key)
- return get_default_or_mapper(key)
- end
- })
-
- -- Hacky way to run after the script is initialized and options (hopefully) added
- mp.add_timeout(0, function()
- -- Handle a '--script-opts identifier-example-config=example.conf' to save an example config to a file
- local example_dump_filename = mp.get_opt(self.identifier .. "-example-config")
- if example_dump_filename then
- self:save_example_options(example_dump_filename)
-
- end
- local explain_config = mp.get_opt(self.identifier .. "-explain-config")
- if explain_config then
- self:explain_options()
- end
-
- if (example_dump_filename or explain_config) and mp.get_property_native("options/idle") then
- msg.info("Exiting.")
- mp.commandv("quit")
- end
- end)
-
- return self
+ local self = setmetatable({}, OptionParser)
+
+ self.identifier = identifier
+ self.config_file = self:_get_config_file(identifier)
+
+ self.OVERRIDE_START = "# Script-saved overrides below this line. Edits will be lost!"
+
+ -- All the options contained, as a list
+ self.options_list = {}
+ -- All the options contained, as a table with keys. See add_option
+ self.options = {}
+
+ self.default_profile = { name = "default", values = {}, loaded = {}, config_lines = {} }
+ self.profiles = {}
+
+ self.active_profile = self.default_profile
+
+ -- Recusing metatable magic to wrap self.values.key.sub_key into
+ -- self.options["key.sub_key"].value, with support for assignments as well
+ function get_value_or_mapper(key)
+ local cur_option = self.options[key]
+
+ if cur_option then
+ -- Wrap tables
+ if cur_option.type == "table" then
+ return setmetatable({}, {
+ __index = function(t, sub_key)
+ return get_value_or_mapper(key .. "." .. sub_key)
+ end,
+ __newindex = function(t, sub_key, value)
+ local sub_option = self.options[key .. "." .. sub_key]
+ if sub_option and sub_option.type ~= "table" then
+ self.active_profile.values[key .. "." .. sub_key] = value
+ end
+ end,
+ })
+ else
+ return self.active_profile.values[key]
+ end
+ end
+ end
+
+ -- Same recusing metatable magic to get the .default
+ function get_default_or_mapper(key)
+ local cur_option = self.options[key]
+
+ if cur_option then
+ if cur_option.type == "table" then
+ return setmetatable({}, {
+ __index = function(t, sub_key)
+ return get_default_or_mapper(key .. "." .. sub_key)
+ end,
+ })
+ else
+ return cur_option.default
+ -- return self.active_profile.values[key]
+ end
+ end
+ end
+
+ -- Easy lookups for values and defaults
+ self.values = setmetatable({}, {
+ __index = function(t, key)
+ return get_value_or_mapper(key)
+ end,
+ __newindex = function(t, key, value)
+ local option = self.options[key]
+ if option then
+ -- option.value = value
+ self.active_profile.values[key] = value
+ end
+ end,
+ })
+
+ self.defaults = setmetatable({}, {
+ __index = function(t, key)
+ return get_default_or_mapper(key)
+ end,
+ })
+
+ -- Hacky way to run after the script is initialized and options (hopefully) added
+ mp.add_timeout(0, function()
+ -- Handle a '--script-opts identifier-example-config=example.conf' to save an example config to a file
+ local example_dump_filename = mp.get_opt(self.identifier .. "-example-config")
+ if example_dump_filename then
+ self:save_example_options(example_dump_filename)
+ end
+ local explain_config = mp.get_opt(self.identifier .. "-explain-config")
+ if explain_config then
+ self:explain_options()
+ end
+
+ if (example_dump_filename or explain_config) and mp.get_property_native("options/idle") then
+ msg.info("Exiting.")
+ mp.commandv("quit")
+ end
+ end)
+
+ return self
end
function OptionParser:activate_profile(profile_name)
- local chosen_profile = nil
- if profile_name then
- for i, profile in ipairs(self.profiles) do
- if profile.name == profile_name then
- chosen_profile = profile
- break
- end
- end
- else
- chosen_profile = self.default_profile
- end
-
- if chosen_profile then
- self.active_profile = chosen_profile
- end
-
+ local chosen_profile = nil
+ if profile_name then
+ for i, profile in ipairs(self.profiles) do
+ if profile.name == profile_name then
+ chosen_profile = profile
+ break
+ end
+ end
+ else
+ chosen_profile = self.default_profile
+ end
+
+ if chosen_profile then
+ self.active_profile = chosen_profile
+ end
end
function OptionParser:add_option(key, default, description, pad_before)
- if self.options[key] ~= nil then
- -- Already exists!
- return nil
- end
-
- local option_index = #self.options_list + 1
- local option_type = type(default)
-
- -- Check if option is an array
- if option_type == "table" then
- if default._array then
- option_type = "array"
- end
- default._array = nil
- end
-
- local option = {
- index = option_index,
- type = option_type,
- key = key,
- default = default,
-
- description = description,
- pad_before = pad_before
- }
-
- self.options_list[option_index] = option
-
- -- table-options are just containers for sub-options and have no value
- if option_type == "table" then
- option.default = nil
-
- -- Add sub-options
- for i, sub_option_data in ipairs(default) do
- local sub_key = sub_option_data[1]
- sub_option_data[1] = key .. "." .. sub_key
- local sub_option = self:add_option(unpack(sub_option_data))
- end
- end
-
- if key then
- self.options[key] = option
- self.default_profile.values[option.key] = option.default
- end
-
- return option
+ if self.options[key] ~= nil then
+ -- Already exists!
+ return nil
+ end
+
+ local option_index = #self.options_list + 1
+ local option_type = type(default)
+
+ -- Check if option is an array
+ if option_type == "table" then
+ if default._array then
+ option_type = "array"
+ end
+ default._array = nil
+ end
+
+ local option = {
+ index = option_index,
+ type = option_type,
+ key = key,
+ default = default,
+
+ description = description,
+ pad_before = pad_before,
+ }
+
+ self.options_list[option_index] = option
+
+ -- table-options are just containers for sub-options and have no value
+ if option_type == "table" then
+ option.default = nil
+
+ -- Add sub-options
+ for i, sub_option_data in ipairs(default) do
+ local sub_key = sub_option_data[1]
+ sub_option_data[1] = key .. "." .. sub_key
+ local sub_option = self:add_option(unpack(sub_option_data))
+ end
+ end
+
+ if key then
+ self.options[key] = option
+ self.default_profile.values[option.key] = option.default
+ end
+
+ return option
end
-
function OptionParser:add_options(list_of_options)
- for i, option_args in ipairs(list_of_options) do
- self:add_option(unpack(option_args))
- end
+ for i, option_args in ipairs(list_of_options) do
+ self:add_option(unpack(option_args))
+ end
end
-
function OptionParser:restore_defaults()
- for key, option in pairs(self.options) do
- if option.type ~= "table" then
- self.active_profile.values[option.key] = option.default
- end
- end
+ for key, option in pairs(self.options) do
+ if option.type ~= "table" then
+ self.active_profile.values[option.key] = option.default
+ end
+ end
end
function OptionParser:restore_loaded()
- for key, option in pairs(self.options) do
- if option.type ~= "table" then
- -- Non-default profiles will have an .loaded entry for all options
- local value = self.active_profile.loaded[option.key]
- if value == nil then value = option.default end
- self.active_profile.values[option.key] = value
- end
- end
+ for key, option in pairs(self.options) do
+ if option.type ~= "table" then
+ -- Non-default profiles will have an .loaded entry for all options
+ local value = self.active_profile.loaded[option.key]
+ if value == nil then
+ value = option.default
+ end
+ self.active_profile.values[option.key] = value
+ end
+ end
end
-
function OptionParser:_get_config_file(identifier)
- local config_filename = "script-opts/" .. identifier .. ".conf"
- local config_file = mp.find_config_file(config_filename)
+ local config_filename = "script-opts/" .. identifier .. ".conf"
+ local config_file = mp.find_config_file(config_filename)
- if not config_file then
- config_filename = "lua-settings/" .. identifier .. ".conf"
- config_file = mp.find_config_file(config_filename)
+ if not config_file then
+ config_filename = "script-opts/" .. identifier .. ".conf"
+ config_file = mp.find_config_file(config_filename)
- if config_file then
- msg.warn("lua-settings/ is deprecated, use directory script-opts/")
- end
- end
+ if config_file then
+ msg.warn("lua-settings/ is deprecated, use directory script-opts/")
+ end
+ end
- return config_file
+ return config_file
end
-
function OptionParser:value_to_string(value)
- if type(value) == "boolean" then
- if value then value = "yes" else value = "no" end
- elseif type(value) == "table" then
- return utils.format_json(value)
- end
- return tostring(value)
+ if type(value) == "boolean" then
+ if value then
+ value = "yes"
+ else
+ value = "no"
+ end
+ elseif type(value) == "table" then
+ return utils.format_json(value)
+ end
+ return tostring(value)
end
-
function OptionParser:string_to_value(option_type, value)
- if option_type == "boolean" then
- if value == "yes" or value == "true" then
- value = true
- elseif value == "no" or value == "false" then
- value = false
- else
- -- can't parse as boolean
- value = nil
- end
- elseif option_type == "number" then
- value = tonumber(value)
- if value == nil then
- -- Can't parse as number
- end
- elseif option_type == "array" then
- value = utils.parse_json(value)
- end
- return value
+ if option_type == "boolean" then
+ if value == "yes" or value == "true" then
+ value = true
+ elseif value == "no" or value == "false" then
+ value = false
+ else
+ -- can't parse as boolean
+ value = nil
+ end
+ elseif option_type == "number" then
+ value = tonumber(value)
+ if value == nil then
+ -- Can't parse as number
+ end
+ elseif option_type == "array" then
+ value = utils.parse_json(value)
+ end
+ return value
end
-
function OptionParser:get_profile(profile_name)
- for i, profile in ipairs(self.profiles) do
- if profile.name == profile_name then
- return profile
- end
- end
+ for i, profile in ipairs(self.profiles) do
+ if profile.name == profile_name then
+ return profile
+ end
+ end
end
-
function OptionParser:create_profile(profile_name, base_on_original)
- if not self:get_profile(profile_name) then
- new_profile = {name = profile_name, values={}, loaded={}, config_lines={}}
-
- if base_on_original then
- -- Copy values from default config
- for k, v in pairs(self.default_profile.values) do
- new_profile.values[k] = v
- end
- for k, v in pairs(self.default_profile.loaded) do
- new_profile.loaded[k] = v
- end
- else
- -- Copy current values, but not loaded
- for k, v in pairs(self.active_profile.values) do
- new_profile.values[k] = v
- end
- end
-
- table.insert(self.profiles, new_profile)
- return new_profile
- end
+ if not self:get_profile(profile_name) then
+ new_profile = { name = profile_name, values = {}, loaded = {}, config_lines = {} }
+
+ if base_on_original then
+ -- Copy values from default config
+ for k, v in pairs(self.default_profile.values) do
+ new_profile.values[k] = v
+ end
+ for k, v in pairs(self.default_profile.loaded) do
+ new_profile.loaded[k] = v
+ end
+ else
+ -- Copy current values, but not loaded
+ for k, v in pairs(self.active_profile.values) do
+ new_profile.values[k] = v
+ end
+ end
+
+ table.insert(self.profiles, new_profile)
+ return new_profile
+ end
end
-
function OptionParser:load_options()
- if not self.config_file then return end
- local file = io.open(self.config_file, 'r')
- if not file then return end
-
- local trim = function(text)
- return (text:gsub("^%s*(.-)%s*$", "%1"))
- end
-
- local current_profile = self.default_profile
- local override_reached = false
- local line_index = 1
-
- -- Read all lines in advance
- local lines = {}
- for line in file:lines() do
- table.insert(lines, line)
- end
- file:close()
-
- local total_lines = #lines
-
- while line_index < total_lines + 1 do
- local line = lines[line_index]
-
- local profile_name = line:match("^%[(..-)%]$")
-
- if line == self.OVERRIDE_START then
- override_reached = true
-
- elseif line:find("#") == 1 then
- -- Skip comments
- elseif profile_name then
- current_profile = self:get_profile(profile_name) or self:create_profile(profile_name, true)
- override_reached = false
-
- else
- local key, value = line:match("^(..-)=(.+)$")
- if key then
- key = trim(key)
- value = trim(value)
-
- local option = self.options[key]
- if not option then
- msg.warn(("%s:%d ignoring unknown key '%s'"):format(self.config_file, line_index, key))
- elseif option.type == "table" then
- msg.warn(("%s:%d ignoring value for table-option %s"):format(self.config_file, line_index, key))
- else
- -- If option is an array, make sure we read all lines
- if option.type == "array" then
- local start_index = line_index
- -- Read lines until one ends with ]
- while not value:match("%]%s*$") do
- line_index = line_index + 1
- if line_index > total_lines then
- msg.error(("%s:%d non-ending %s for key '%s'"):format(self.config_file, start_index, option.type, key))
- end
- value = value .. trim(lines[line_index])
- end
- end
- local parsed_value = self:string_to_value(option.type, value)
-
- if parsed_value == nil then
- msg.error(("%s:%d error parsing value '%s' for key '%s' (as %s)"):format(self.config_file, line_index, value, key, option.type))
- else
- current_profile.values[option.key] = parsed_value
- if not override_reached then
- current_profile.loaded[option.key] = parsed_value
- end
- end
- end
- end
- end
-
- if not override_reached and not profile_name then
- table.insert(current_profile.config_lines, line)
- end
-
- line_index = line_index + 1
- end
+ if not self.config_file then
+ return
+ end
+ local file = io.open(self.config_file, "r")
+ if not file then
+ return
+ end
+
+ local trim = function(text)
+ return (text:gsub("^%s*(.-)%s*$", "%1"))
+ end
+
+ local current_profile = self.default_profile
+ local override_reached = false
+ local line_index = 1
+
+ -- Read all lines in advance
+ local lines = {}
+ for line in file:lines() do
+ table.insert(lines, line)
+ end
+ file:close()
+
+ local total_lines = #lines
+
+ while line_index < total_lines + 1 do
+ local line = lines[line_index]
+
+ local profile_name = line:match("^%[(..-)%]$")
+
+ if line == self.OVERRIDE_START then
+ override_reached = true
+ elseif line:find("#") == 1 then
+ -- Skip comments
+ elseif profile_name then
+ current_profile = self:get_profile(profile_name) or self:create_profile(profile_name, true)
+ override_reached = false
+ else
+ local key, value = line:match("^(..-)=(.+)$")
+ if key then
+ key = trim(key)
+ value = trim(value)
+
+ local option = self.options[key]
+ if not option then
+ msg.warn(("%s:%d ignoring unknown key '%s'"):format(self.config_file, line_index, key))
+ elseif option.type == "table" then
+ msg.warn(("%s:%d ignoring value for table-option %s"):format(self.config_file, line_index, key))
+ else
+ -- If option is an array, make sure we read all lines
+ if option.type == "array" then
+ local start_index = line_index
+ -- Read lines until one ends with ]
+ while not value:match("%]%s*$") do
+ line_index = line_index + 1
+ if line_index > total_lines then
+ msg.error(
+ ("%s:%d non-ending %s for key '%s'"):format(
+ self.config_file,
+ start_index,
+ option.type,
+ key
+ )
+ )
+ end
+ value = value .. trim(lines[line_index])
+ end
+ end
+ local parsed_value = self:string_to_value(option.type, value)
+
+ if parsed_value == nil then
+ msg.error(
+ ("%s:%d error parsing value '%s' for key '%s' (as %s)"):format(
+ self.config_file,
+ line_index,
+ value,
+ key,
+ option.type
+ )
+ )
+ else
+ current_profile.values[option.key] = parsed_value
+ if not override_reached then
+ current_profile.loaded[option.key] = parsed_value
+ end
+ end
+ end
+ end
+ end
+
+ if not override_reached and not profile_name then
+ table.insert(current_profile.config_lines, line)
+ end
+
+ line_index = line_index + 1
+ end
end
-
function OptionParser:save_options()
- if not self.config_file then return nil, "no configuration file found" end
-
- local file = io.open(self.config_file, 'w')
- if not file then return nil, "unable to open configuration file for writing" end
-
- local profiles = {self.default_profile}
- for i, profile in ipairs(self.profiles) do
- table.insert(profiles, profile)
- end
-
- local out_lines = {}
-
- local add_linebreak = function()
- if out_lines[#out_lines] ~= '' then
- table.insert(out_lines, '')
- end
- end
-
- for profile_index, profile in ipairs(profiles) do
-
- local profile_override_lines = {}
- for option_index, option in ipairs(self.options_list) do
- local option_value = profile.values[option.key]
- local option_loaded = profile.loaded[option.key]
-
- if option_loaded == nil then
- option_loaded = self.default_profile.loaded[option.key]
- end
- if option_loaded == nil then
- option_loaded = option.default
- end
-
- -- If value is different from default AND loaded value, store it in array
- if option.key then
- if (option_value ~= option_loaded) then
- table.insert(profile_override_lines, ('%s=%s'):format(option.key, self:value_to_string(option_value)))
- end
- end
- end
-
- if (#profile.config_lines > 0 or #profile_override_lines > 0) and profile ~= self.default_profile then
- -- Write profile name, if this is not default profile
- add_linebreak()
- table.insert(out_lines, ("[%s]"):format(profile.name))
- end
-
- -- Write original config lines
- for line_index, line in ipairs(profile.config_lines) do
- table.insert(out_lines, line)
- end
- -- end
-
- if #profile_override_lines > 0 then
- -- Add another newline before the override comment, if needed
- add_linebreak()
-
- table.insert(out_lines, self.OVERRIDE_START)
- for override_line_index, override_line in ipairs(profile_override_lines) do
- table.insert(out_lines, override_line)
- end
- end
-
- end
-
- -- Add a final linebreak if needed
- add_linebreak()
-
- file:write(table.concat(out_lines, "\n"))
- file:close()
-
- return true
+ if not self.config_file then
+ return nil, "no configuration file found"
+ end
+
+ local file = io.open(self.config_file, "w")
+ if not file then
+ return nil, "unable to open configuration file for writing"
+ end
+
+ local profiles = { self.default_profile }
+ for i, profile in ipairs(self.profiles) do
+ table.insert(profiles, profile)
+ end
+
+ local out_lines = {}
+
+ local add_linebreak = function()
+ if out_lines[#out_lines] ~= "" then
+ table.insert(out_lines, "")
+ end
+ end
+
+ for profile_index, profile in ipairs(profiles) do
+ local profile_override_lines = {}
+ for option_index, option in ipairs(self.options_list) do
+ local option_value = profile.values[option.key]
+ local option_loaded = profile.loaded[option.key]
+
+ if option_loaded == nil then
+ option_loaded = self.default_profile.loaded[option.key]
+ end
+ if option_loaded == nil then
+ option_loaded = option.default
+ end
+
+ -- If value is different from default AND loaded value, store it in array
+ if option.key then
+ if option_value ~= option_loaded then
+ table.insert(
+ profile_override_lines,
+ ("%s=%s"):format(option.key, self:value_to_string(option_value))
+ )
+ end
+ end
+ end
+
+ if (#profile.config_lines > 0 or #profile_override_lines > 0) and profile ~= self.default_profile then
+ -- Write profile name, if this is not default profile
+ add_linebreak()
+ table.insert(out_lines, ("[%s]"):format(profile.name))
+ end
+
+ -- Write original config lines
+ for line_index, line in ipairs(profile.config_lines) do
+ table.insert(out_lines, line)
+ end
+ -- end
+
+ if #profile_override_lines > 0 then
+ -- Add another newline before the override comment, if needed
+ add_linebreak()
+
+ table.insert(out_lines, self.OVERRIDE_START)
+ for override_line_index, override_line in ipairs(profile_override_lines) do
+ table.insert(out_lines, override_line)
+ end
+ end
+ end
+
+ -- Add a final linebreak if needed
+ add_linebreak()
+
+ file:write(table.concat(out_lines, "\n"))
+ file:close()
+
+ return true
end
-
function OptionParser:get_default_config_lines()
- local example_config_lines = {}
+ local example_config_lines = {}
- for option_index, option in ipairs(self.options_list) do
- if option.pad_before then
- table.insert(example_config_lines, '')
- end
+ for option_index, option in ipairs(self.options_list) do
+ if option.pad_before then
+ table.insert(example_config_lines, "")
+ end
- if option.description then
- for description_line in option.description:gmatch('[^\r\n]+') do
- table.insert(example_config_lines, ('# ' .. description_line))
- end
- end
- if option.key and option.type ~= "table" then
- table.insert(example_config_lines, ('%s=%s'):format(option.key, self:value_to_string(option.default)) )
- end
- end
- return example_config_lines
+ if option.description then
+ for description_line in option.description:gmatch("[^\r\n]+") do
+ table.insert(example_config_lines, ("# " .. description_line))
+ end
+ end
+ if option.key and option.type ~= "table" then
+ table.insert(example_config_lines, ("%s=%s"):format(option.key, self:value_to_string(option.default)))
+ end
+ end
+ return example_config_lines
end
-
function OptionParser:explain_options()
- local example_config_lines = self:get_default_config_lines()
- msg.info(table.concat(example_config_lines, '\n'))
+ local example_config_lines = self:get_default_config_lines()
+ msg.info(table.concat(example_config_lines, "\n"))
end
-
function OptionParser:save_example_options(filename)
- local file = io.open(filename, "w")
- if not file then
- msg.error("Unable to open file '" .. filename .. "' for writing")
- else
- local example_config_lines = self:get_default_config_lines()
- file:write(table.concat(example_config_lines, '\n'))
- file:close()
- msg.info("Wrote example config to file '" .. filename .. "'")
- end
+ local file = io.open(filename, "w")
+ if not file then
+ msg.error("Unable to open file '" .. filename .. "' for writing")
+ else
+ local example_config_lines = self:get_default_config_lines()
+ file:write(table.concat(example_config_lines, "\n"))
+ file:close()
+ msg.info("Wrote example config to file '" .. filename .. "'")
+ end
end
local SCRIPT_NAME = "mpv_crop_script"
@@ -1232,11 +1285,17 @@ local script_options = OptionParser(SCRIPT_NAME)
local option_values = script_options.values
script_options:add_options({
- {nil, nil, "mpv_crop_script.lua options and default values"},
- {nil, nil, "Output options #", true},
- {"output_template", "${filename}${!is_image: ${#pos:%02h.%02m.%06.3s}}${!full: ${crop_w}x${crop_h}} ${%unique:%03d}.${ext}",
- "Filename output template. See README.md for property expansion documentation."},
- {nil, nil, [[Script-provided properties:
+ { nil, nil, "mpv_crop_script.lua options and default values" },
+ { nil, nil, "Output options #", true },
+ {
+ "output_template",
+ "${filename}${!is_image: ${#pos:%02h.%02m.%06.3s}}${!full: ${crop_w}x${crop_h}} ${%unique:%03d}.${ext}",
+ "Filename output template. See README.md for property expansion documentation.",
+ },
+ {
+ nil,
+ nil,
+ [[Script-provided properties:
filename - filename without extension
file_ext - original extension without leading dot
path - original file path
@@ -1250,39 +1309,47 @@ script_options:add_options({
crop_y2 - bottom
full - boolean denoting a full (temporary) screenshot instead of crop
is_image - boolean denoting the source file is likely an image (zero duration and position)
- unique - counter that will increase per each existing filename, until a unique name is found]]},
-
- {"output_format", "png",
- "Format (encoder) to save final crops in. For example, png, mjpeg, targa, bmp"},
- {"output_extension", "",
- "Output extension. Leave blank to try to choose from the encoder (if supported)"},
-
- {"create_directories", false,
- "Whether to create the directories in the final output path (defined by output_template)"},
- {"skip_screenshot_for_images", true,
- "If the current file is an image, skip taking a temporary screenshot and crop the image directly"},
- {"keep_original", false,
- "Keep the full-sized temporary screenshot as well"},
-
- {nil, nil, "Crop tool options #", true},
- {"overlay_transparency", 160,
- "Transparency (0 - opaque, 255 - transparent) of the dim overlay on the non-cropped area"},
- {"overlay_lightness", 0,
- "Ligthness (0 - black, 255 - white) of the dim overlay on the non-cropped area"},
- {"draw_mouse", false,
- "Draw the crop crosshair"},
- {"guide_type", "none",
- "Crop guide type. One of: none, grid, center"},
- {"color_invert", false,
- "Use black lines instead of white for the crop frame and crosshair"},
- {"auto_invert", false,
- "Try to check if video is light or dark upon opening crop tool, and invert the colors if necessary"},
-
- {nil, nil, "Misc options #", true},
- {"warn_about_template", true,
- "Warn about output_template missing ${ext}, to ensure the extension is not missing"},
- {"disable_keybind", false,
- "Disable the built-in keybind"}
+ unique - counter that will increase per each existing filename, until a unique name is found]],
+ },
+
+ { "output_format", "png", "Format (encoder) to save final crops in. For example, png, mjpeg, targa, bmp" },
+ { "output_extension", "", "Output extension. Leave blank to try to choose from the encoder (if supported)" },
+
+ {
+ "create_directories",
+ false,
+ "Whether to create the directories in the final output path (defined by output_template)",
+ },
+ {
+ "skip_screenshot_for_images",
+ true,
+ "If the current file is an image, skip taking a temporary screenshot and crop the image directly",
+ },
+ { "keep_original", false, "Keep the full-sized temporary screenshot as well" },
+
+ { nil, nil, "Crop tool options #", true },
+ {
+ "overlay_transparency",
+ 160,
+ "Transparency (0 - opaque, 255 - transparent) of the dim overlay on the non-cropped area",
+ },
+ { "overlay_lightness", 0, "Ligthness (0 - black, 255 - white) of the dim overlay on the non-cropped area" },
+ { "draw_mouse", false, "Draw the crop crosshair" },
+ { "guide_type", "none", "Crop guide type. One of: none, grid, center" },
+ { "color_invert", false, "Use black lines instead of white for the crop frame and crosshair" },
+ {
+ "auto_invert",
+ false,
+ "Try to check if video is light or dark upon opening crop tool, and invert the colors if necessary",
+ },
+
+ { nil, nil, "Misc options #", true },
+ {
+ "warn_about_template",
+ true,
+ "Warn about output_template missing ${ext}, to ensure the extension is not missing",
+ },
+ { "disable_keybind", false, "Disable the built-in keybind" },
})
-- Read user-given options, if any
@@ -1294,208 +1361,230 @@ script_options:load_options()
mpv itself does (video/out/aspect.c).
Does not depend on other libs.
-]]--
+]]
+--
local DisplayState = {}
DisplayState.__index = DisplayState
setmetatable(DisplayState, {
- __call = function (cls, ...) return cls.new(...) end
+ __call = function(cls, ...)
+ return cls.new(...)
+ end,
})
function DisplayState.new()
- local self = setmetatable({}, DisplayState)
+ local self = setmetatable({}, DisplayState)
- self:reset()
+ self:reset()
- return self
+ return self
end
function DisplayState:reset()
- self.screen = {} -- Display (window, fullscreen) size
- self.video = {} -- Video size
- self.scale = {} -- video / screen
- self.bounds = {} -- Video rect within display
+ self.screen = {} -- Display (window, fullscreen) size
+ self.video = {} -- Video size
+ self.scale = {} -- video / screen
+ self.bounds = {} -- Video rect within display
- self.screen_ready = false
- self.video_ready = false
+ self.screen_ready = false
+ self.video_ready = false
- -- Stores internal display state (panscan, align, zoom etc)
- self.current_state = nil
+ -- Stores internal display state (panscan, align, zoom etc)
+ self.current_state = nil
end
function DisplayState:setup_events()
- mp.register_event("file-loaded", function() self:event_file_loaded() end)
+ mp.register_event("file-loaded", function()
+ self:event_file_loaded()
+ end)
end
function DisplayState:event_file_loaded()
- self:reset()
- self:recalculate_bounds(true)
+ self:reset()
+ self:recalculate_bounds(true)
end
-- Turns screen-space XY to video XY (can go negative)
function DisplayState:screen_to_video(x, y)
- local nx = (x - self.bounds.left) * self.scale.x
- local ny = (y - self.bounds.top ) * self.scale.y
- return nx, ny
+ local nx = (x - self.bounds.left) * self.scale.x
+ local ny = (y - self.bounds.top) * self.scale.y
+ return nx, ny
end
-- Turns video-space XY to screen XY
function DisplayState:video_to_screen(x, y)
- local nx = (x / self.scale.x) + self.bounds.left
- local ny = (y / self.scale.y) + self.bounds.top
- return nx, ny
+ local nx = (x / self.scale.x) + self.bounds.left
+ local ny = (y / self.scale.y) + self.bounds.top
+ return nx, ny
end
function DisplayState:_collect_display_state()
- local screen_w, screen_h, screen_aspect = mp.get_osd_size()
+ local screen_w, screen_h, screen_aspect = mp.get_osd_size()
- local state = {
- screen_w = screen_w,
- screen_h = screen_h,
- screen_aspect = screen_aspect,
+ local state = {
+ screen_w = screen_w,
+ screen_h = screen_h,
+ screen_aspect = screen_aspect,
- video_w = mp.get_property_native("dwidth"),
- video_h = mp.get_property_native("dheight"),
+ video_w = mp.get_property_native("dwidth"),
+ video_h = mp.get_property_native("dheight"),
- video_w_raw = mp.get_property_native("video-out-params/w"),
- video_h_raw = mp.get_property_native("video-out-params/h"),
+ video_w_raw = mp.get_property_native("video-out-params/w"),
+ video_h_raw = mp.get_property_native("video-out-params/h"),
- panscan = mp.get_property_native("panscan"),
- video_zoom = mp.get_property_native("video-zoom"),
- video_unscaled = mp.get_property_native("video-unscaled"),
+ panscan = mp.get_property_native("panscan"),
+ video_zoom = mp.get_property_native("video-zoom"),
+ video_unscaled = mp.get_property_native("video-unscaled"),
- video_align_x = mp.get_property_native("video-align-x"),
- video_align_y = mp.get_property_native("video-align-y"),
+ video_align_x = mp.get_property_native("video-align-x"),
+ video_align_y = mp.get_property_native("video-align-y"),
- video_pan_x = mp.get_property_native("video-pan-x"),
- video_pan_y = mp.get_property_native("video-pan-y"),
+ video_pan_x = mp.get_property_native("video-pan-x"),
+ video_pan_y = mp.get_property_native("video-pan-y"),
- fullscreen = mp.get_property_native("fullscreen"),
- keepaspect = mp.get_property_native("keepaspect"),
- keepaspect_window = mp.get_property_native("keepaspect-window")
- }
+ fullscreen = mp.get_property_native("fullscreen"),
+ keepaspect = mp.get_property_native("keepaspect"),
+ keepaspect_window = mp.get_property_native("keepaspect-window"),
+ }
- return state
+ return state
end
function DisplayState:_state_changed(state)
- if self.current_state == nil then return true end
+ if self.current_state == nil then
+ return true
+ end
- for k in pairs(state) do
- if state[k] ~= self.current_state[k] then return true end
- end
- return false
+ for k in pairs(state) do
+ if state[k] ~= self.current_state[k] then
+ return true
+ end
+ end
+ return false
end
-
function DisplayState:recalculate_bounds(forced)
- local new_state = self:_collect_display_state()
- if not (forced or self:_state_changed(new_state)) then
- -- Early out
- return self.screen_ready
- end
- self.current_state = new_state
-
- -- Store screen dimensions
- self.screen.width = new_state.screen_w
- self.screen.height = new_state.screen_h
- self.screen.ratio = new_state.screen_w / new_state.screen_h
- self.screen_ready = true
-
- -- Video dimensions
- if new_state.video_w and new_state.video_h then
- self.video.width = new_state.video_w
- self.video.height = new_state.video_h
- self.video.ratio = new_state.video_w / new_state.video_h
-
- -- This magic has been adapted from mpv's own video/out/aspect.c
-
- if new_state.keepaspect then
- local scaled_w, scaled_h = self:_aspect_calc_panscan(new_state)
- local video_left, video_right = self:_split_scaling(new_state.screen_w, scaled_w, new_state.video_zoom, new_state.video_align_x, new_state.video_pan_x)
- local video_top, video_bottom = self:_split_scaling(new_state.screen_h, scaled_h, new_state.video_zoom, new_state.video_align_y, new_state.video_pan_y)
- self.bounds = {
- left = video_left,
- right = video_right,
-
- top = video_top,
- bottom = video_bottom,
-
- width = video_right - video_left,
- height = video_bottom - video_top,
- }
- else
- self.bounds = {
- left = 0,
- top = 0,
- right = self.screen.width,
- bottom = self.screen.height,
-
- width = self.screen.width,
- height = self.screen.height,
- }
- end
-
- self.scale.x = new_state.video_w_raw / self.bounds.width
- self.scale.y = new_state.video_h_raw / self.bounds.height
-
- self.video_ready = true
- end
-
- return self.screen_ready
+ local new_state = self:_collect_display_state()
+ if not (forced or self:_state_changed(new_state)) then
+ -- Early out
+ return self.screen_ready
+ end
+ self.current_state = new_state
+
+ -- Store screen dimensions
+ self.screen.width = new_state.screen_w
+ self.screen.height = new_state.screen_h
+ self.screen.ratio = new_state.screen_w / new_state.screen_h
+ self.screen_ready = true
+
+ -- Video dimensions
+ if new_state.video_w and new_state.video_h then
+ self.video.width = new_state.video_w
+ self.video.height = new_state.video_h
+ self.video.ratio = new_state.video_w / new_state.video_h
+
+ -- This magic has been adapted from mpv's own video/out/aspect.c
+
+ if new_state.keepaspect then
+ local scaled_w, scaled_h = self:_aspect_calc_panscan(new_state)
+ local video_left, video_right = self:_split_scaling(
+ new_state.screen_w,
+ scaled_w,
+ new_state.video_zoom,
+ new_state.video_align_x,
+ new_state.video_pan_x
+ )
+ local video_top, video_bottom = self:_split_scaling(
+ new_state.screen_h,
+ scaled_h,
+ new_state.video_zoom,
+ new_state.video_align_y,
+ new_state.video_pan_y
+ )
+ self.bounds = {
+ left = video_left,
+ right = video_right,
+
+ top = video_top,
+ bottom = video_bottom,
+
+ width = video_right - video_left,
+ height = video_bottom - video_top,
+ }
+ else
+ self.bounds = {
+ left = 0,
+ top = 0,
+ right = self.screen.width,
+ bottom = self.screen.height,
+
+ width = self.screen.width,
+ height = self.screen.height,
+ }
+ end
+
+ self.scale.x = new_state.video_w_raw / self.bounds.width
+ self.scale.y = new_state.video_h_raw / self.bounds.height
+
+ self.video_ready = true
+ end
+
+ return self.screen_ready
end
-
function DisplayState:_aspect_calc_panscan(state)
- -- From video/out/aspect.c
- local f_width = state.screen_w
- local f_height = (state.screen_w / state.video_w) * state.video_h
-
- if f_height > state.screen_h or f_height < state.video_h_raw then
- local tmp_w = (state.screen_h / state.video_h) * state.video_w
- if tmp_w <= state.screen_w then
- f_height = state.screen_h
- f_width = tmp_w
- end
- end
-
- local vo_panscan_area = state.screen_h - f_height
-
- local f_w = f_width / f_height
- local f_h = 1
- if (vo_panscan_area == 0) then
- vo_panscan_area = state.screen_w - f_width
- f_w = 1
- f_h = f_height / f_width
- end
-
- if state.video_unscaled then
- vo_panscan_area = 0
- if state.video_unscaled ~= "downscale-big" or ((state.video_w <= state.screen_w) and (state.video_h <= state.screen_h)) then
- f_width = state.video_w
- f_height = state.video_h
- end
- end
-
- local scaled_w = math.floor( f_width + vo_panscan_area * state.panscan * f_w )
- local scaled_h = math.floor( f_height + vo_panscan_area * state.panscan * f_h )
- return scaled_w, scaled_h
+ -- From video/out/aspect.c
+ local f_width = state.screen_w
+ local f_height = (state.screen_w / state.video_w) * state.video_h
+
+ if f_height > state.screen_h or f_height < state.video_h_raw then
+ local tmp_w = (state.screen_h / state.video_h) * state.video_w
+ if tmp_w <= state.screen_w then
+ f_height = state.screen_h
+ f_width = tmp_w
+ end
+ end
+
+ local vo_panscan_area = state.screen_h - f_height
+
+ local f_w = f_width / f_height
+ local f_h = 1
+ if vo_panscan_area == 0 then
+ vo_panscan_area = state.screen_w - f_width
+ f_w = 1
+ f_h = f_height / f_width
+ end
+
+ if state.video_unscaled then
+ vo_panscan_area = 0
+ if
+ state.video_unscaled ~= "downscale-big"
+ or ((state.video_w <= state.screen_w) and (state.video_h <= state.screen_h))
+ then
+ f_width = state.video_w
+ f_height = state.video_h
+ end
+ end
+
+ local scaled_w = math.floor(f_width + vo_panscan_area * state.panscan * f_w)
+ local scaled_h = math.floor(f_height + vo_panscan_area * state.panscan * f_h)
+ return scaled_w, scaled_h
end
function DisplayState:_split_scaling(dst_size, scaled_src_size, zoom, align, pan)
- -- From video/out/aspect.c as well
- scaled_src_size = math.floor(scaled_src_size * 2^zoom)
- align = (align + 1) / 2
+ -- From video/out/aspect.c as well
+ scaled_src_size = math.floor(scaled_src_size * 2 ^ zoom)
+ align = (align + 1) / 2
- local dst_start = (dst_size - scaled_src_size) * align + pan * scaled_src_size
- local dst_end = dst_start + scaled_src_size
+ local dst_start = (dst_size - scaled_src_size) * align + pan * scaled_src_size
+ local dst_end = dst_start + scaled_src_size
- -- We don't actually want these - we want to go out of bounds!
- -- dst_start = math.max(0, dst_start)
- -- dst_end = math.min(dst_size, dst_end)
+ -- We don't actually want these - we want to go out of bounds!
+ -- dst_start = math.max(0, dst_start)
+ -- dst_end = math.min(dst_size, dst_end)
- return math.floor(dst_start), math.floor(dst_end)
+ return math.floor(dst_start), math.floor(dst_end)
end
--[[
ASSCropper is a tool to get crop values with a visual tool
@@ -1503,1220 +1592,1358 @@ end
with a crosshair, guides, etc.
Indirectly depends on DisplayState (as a given instance).
-]]--
+]]
+--
local ASSCropper = {}
ASSCropper.__index = ASSCropper
setmetatable(ASSCropper, {
- __call = function (cls, ...) return cls.new(...) end
+ __call = function(cls, ...)
+ return cls.new(...)
+ end,
})
function ASSCropper.new(display_state)
- local self = setmetatable({}, ASSCropper)
- local script_name = mp.get_script_name()
- self.keybind_group = script_name .. "_asscropper_binds"
- self.cropdetect_label = script_name .. "_asscropper_cropdetect"
- self.blackframe_label = script_name .. "_asscropper_blackframe"
- self.crop_label = script_name .. "_asscropper_crop"
-
- self.display_state = display_state
-
- self.tick_callback = nil
- self.tick_timer = mp.add_periodic_timer(1/60, function()
- if self.tick_callback then self.tick_callback() end
- end)
- self.tick_timer:stop()
-
- self.text_size = 18
-
- self.overlay_transparency = 160
- self.overlay_lightness = 0
-
- self.corner_size = 40
- self.corner_required_size = self.corner_size * 3
-
- self.guide_type_names = {
- [0] = "No guides",
- [1] = "Grid guides",
- [2] = "Center guides"
- }
- self.guide_type_count = 3
-
- self.default_options = {
- even_dimensions = false,
- guide_type = 0,
- draw_mouse = false,
- draw_help = true,
- color_invert = false,
- auto_invert = false,
- }
- self.options = default_options
-
- self.active = false
-
- self.mouse_screen = {x=0, y=0}
- self.mouse_video = {x=0, y=0}
-
- -- Crop in video-space
- self.current_crop = nil
-
- self.dragging = 0
- self.drag_start = {x=0, y=0}
- self.restrict_ratio = false
-
- self.testing_crop = false
-
- self.detecting_crop = nil
- self.cropdetect_wait = nil
- self.cropdetect_timeout = nil
-
- self.detecting_blackframe = nil
- self.blackframe_wait = nil
- self.blackframe_timeout = nil
-
- self.nudges = {
- NUDGE_LEFT = {-1, 0, -1, 0},
- NUDGE_UP = { 0, -1, 0, -1},
- NUDGE_RIGHT = { 1, 0, 1, 0},
- NUDGE_DOWN = { 0, 1, 0, 1}
- }
-
- self.resizes = {
- SHRINK_LEFT = { 1, 0, 0, 0},
- SHRINK_TOP = { 0, 1, 0, 0},
- SHRINK_RIGHT = { 0, 0, -1, 0},
- SHRINK_BOT = { 0, 0, 0, -1},
-
- GROW_LEFT = {-1, 0, 0, 0},
- GROW_TOP = { 0, -1, 0, 0},
- GROW_RIGHT = { 0, 0, 1, 0},
- GROW_BOT = { 0, 0, 0, 1},
- }
-
- self._key_binds = {
- {"mouse_move", function() self:update_mouse_position() end },
- {"mouse_btn0", function(e) self:on_mouse("mouse_btn0", e) end, {complex=true}},
- {"shift+mouse_btn0", function(e) self:on_mouse("mouse_btn0", e, true) end, {complex=true}},
-
- {"c", function() self:key_event("CROSSHAIR") end },
- {"d", function() self:key_event("CROP_DETECT") end },
- {"x", function() self:key_event("GUIDES") end },
- {"t", function() self:key_event("TEST") end },
- {"z", function() self:key_event("INVERT") end },
-
- {"shift+left", function() self:key_event("NUDGE_LEFT") end, {repeatable=true} },
- {"shift+up", function() self:key_event("NUDGE_UP") end, {repeatable=true} },
- {"shift+right", function() self:key_event("NUDGE_RIGHT") end, {repeatable=true} },
- {"shift+down", function() self:key_event("NUDGE_DOWN") end, {repeatable=true} },
-
- {"ctrl+left", function() self:key_event("GROW_LEFT") end, {repeatable=true} },
- {"ctrl+up", function() self:key_event("GROW_TOP") end, {repeatable=true} },
- {"ctrl+right", function() self:key_event("SHRINK_LEFT") end, {repeatable=true} },
- {"ctrl+down", function() self:key_event("SHRINK_TOP") end, {repeatable=true} },
-
- {"ctrl+shift+left", function() self:key_event("SHRINK_RIGHT") end, {repeatable=true} },
- {"ctrl+shift+up", function() self:key_event("SHRINK_BOT") end, {repeatable=true} },
- {"ctrl+shift+right", function() self:key_event("GROW_RIGHT") end, {repeatable=true} },
- {"ctrl+shift+down", function() self:key_event("GROW_BOT") end, {repeatable=true} },
-
- {"ENTER", function() self:key_event("ENTER") end },
- {"ESC", function() self:key_event("ESC") end }
- }
-
- self._keys_bound = false
-
- for k, v in pairs(self._key_binds) do
- -- Insert a key name into the tables
- table.insert(v, 2, self.keybind_group .. "_key_" .. v[1])
- end
-
- return self
+ local self = setmetatable({}, ASSCropper)
+ local script_name = mp.get_script_name()
+ self.keybind_group = script_name .. "_asscropper_binds"
+ self.cropdetect_label = script_name .. "_asscropper_cropdetect"
+ self.blackframe_label = script_name .. "_asscropper_blackframe"
+ self.crop_label = script_name .. "_asscropper_crop"
+
+ self.display_state = display_state
+
+ self.tick_callback = nil
+ self.tick_timer = mp.add_periodic_timer(1 / 60, function()
+ if self.tick_callback then
+ self.tick_callback()
+ end
+ end)
+ self.tick_timer:stop()
+
+ self.text_size = 18
+
+ self.overlay_transparency = 160
+ self.overlay_lightness = 0
+
+ self.corner_size = 40
+ self.corner_required_size = self.corner_size * 3
+
+ self.guide_type_names = {
+ [0] = "No guides",
+ [1] = "Grid guides",
+ [2] = "Center guides",
+ }
+ self.guide_type_count = 3
+
+ self.default_options = {
+ even_dimensions = false,
+ guide_type = 0,
+ draw_mouse = false,
+ draw_help = true,
+ color_invert = false,
+ auto_invert = false,
+ }
+ self.options = default_options
+
+ self.active = false
+
+ self.mouse_screen = { x = 0, y = 0 }
+ self.mouse_video = { x = 0, y = 0 }
+
+ -- Crop in video-space
+ self.current_crop = nil
+
+ self.dragging = 0
+ self.drag_start = { x = 0, y = 0 }
+ self.restrict_ratio = false
+
+ self.testing_crop = false
+
+ self.detecting_crop = nil
+ self.cropdetect_wait = nil
+ self.cropdetect_timeout = nil
+
+ self.detecting_blackframe = nil
+ self.blackframe_wait = nil
+ self.blackframe_timeout = nil
+
+ self.nudges = {
+ NUDGE_LEFT = { -1, 0, -1, 0 },
+ NUDGE_UP = { 0, -1, 0, -1 },
+ NUDGE_RIGHT = { 1, 0, 1, 0 },
+ NUDGE_DOWN = { 0, 1, 0, 1 },
+ }
+
+ self.resizes = {
+ SHRINK_LEFT = { 1, 0, 0, 0 },
+ SHRINK_TOP = { 0, 1, 0, 0 },
+ SHRINK_RIGHT = { 0, 0, -1, 0 },
+ SHRINK_BOT = { 0, 0, 0, -1 },
+
+ GROW_LEFT = { -1, 0, 0, 0 },
+ GROW_TOP = { 0, -1, 0, 0 },
+ GROW_RIGHT = { 0, 0, 1, 0 },
+ GROW_BOT = { 0, 0, 0, 1 },
+ }
+
+ self._key_binds = {
+ {
+ "mouse_move",
+ function()
+ self:update_mouse_position()
+ end,
+ },
+ {
+ "mouse_btn0",
+ function(e)
+ self:on_mouse("mouse_btn0", e)
+ end,
+ { complex = true },
+ },
+ {
+ "shift+mouse_btn0",
+ function(e)
+ self:on_mouse("mouse_btn0", e, true)
+ end,
+ { complex = true },
+ },
+
+ {
+ "ctrl+shift+c",
+ function()
+ self:key_event("CROSSHAIR")
+ end,
+ },
+ {
+ "ctrl+shift+d",
+ function()
+ self:key_event("CROP_DETECT")
+ end,
+ },
+ {
+ "ctrl+shift+x",
+ function()
+ self:key_event("GUIDES")
+ end,
+ },
+ {
+ "ctrl+shift+t",
+ function()
+ self:key_event("TEST")
+ end,
+ },
+ {
+ "ctrl+shift+z",
+ function()
+ self:key_event("INVERT")
+ end,
+ },
+
+ {
+ "shift+left",
+ function()
+ self:key_event("NUDGE_LEFT")
+ end,
+ { repeatable = true },
+ },
+ {
+ "shift+up",
+ function()
+ self:key_event("NUDGE_UP")
+ end,
+ { repeatable = true },
+ },
+ {
+ "shift+right",
+ function()
+ self:key_event("NUDGE_RIGHT")
+ end,
+ { repeatable = true },
+ },
+ {
+ "shift+down",
+ function()
+ self:key_event("NUDGE_DOWN")
+ end,
+ { repeatable = true },
+ },
+
+ {
+ "ctrl+left",
+ function()
+ self:key_event("GROW_LEFT")
+ end,
+ { repeatable = true },
+ },
+ {
+ "ctrl+up",
+ function()
+ self:key_event("GROW_TOP")
+ end,
+ { repeatable = true },
+ },
+ {
+ "ctrl+right",
+ function()
+ self:key_event("SHRINK_LEFT")
+ end,
+ { repeatable = true },
+ },
+ {
+ "ctrl+down",
+ function()
+ self:key_event("SHRINK_TOP")
+ end,
+ { repeatable = true },
+ },
+
+ {
+ "ctrl+shift+left",
+ function()
+ self:key_event("SHRINK_RIGHT")
+ end,
+ { repeatable = true },
+ },
+ {
+ "ctrl+shift+up",
+ function()
+ self:key_event("SHRINK_BOT")
+ end,
+ { repeatable = true },
+ },
+ {
+ "ctrl+shift+right",
+ function()
+ self:key_event("GROW_RIGHT")
+ end,
+ { repeatable = true },
+ },
+ {
+ "ctrl+shift+down",
+ function()
+ self:key_event("GROW_BOT")
+ end,
+ { repeatable = true },
+ },
+
+ {
+ "ENTER",
+ function()
+ self:key_event("ENTER")
+ end,
+ },
+ {
+ "ESC",
+ function()
+ self:key_event("ESC")
+ end,
+ },
+ }
+
+ self._keys_bound = false
+
+ for k, v in pairs(self._key_binds) do
+ -- Insert a key name into the tables
+ table.insert(v, 2, self.keybind_group .. "_key_" .. v[1])
+ end
+
+ return self
end
function ASSCropper:enable_key_bindings()
- if not self._keys_bound then
- for k, v in pairs(self._key_binds) do
- mp.add_forced_key_binding(unpack(v))
- end
- -- Clear "allow-vo-dragging"
- mp.input_enable_section("input_forced_" .. mp.script_name)
- self._keys_bound = true
- end
+ if not self._keys_bound then
+ for k, v in pairs(self._key_binds) do
+ mp.add_forced_key_binding(unpack(v))
+ end
+ -- Clear "allow-vo-dragging"
+ mp.input_enable_section("input_forced_" .. mp.script_name)
+ self._keys_bound = true
+ end
end
function ASSCropper:disable_key_bindings()
- for k, v in pairs(self._key_binds) do
- mp.remove_key_binding(v[2]) -- remove by name
- end
- self._keys_bound = false
+ for k, v in pairs(self._key_binds) do
+ mp.remove_key_binding(v[2]) -- remove by name
+ end
+ self._keys_bound = false
end
-
function ASSCropper:finalize_crop()
- if self.current_crop ~= nil then
- local x1, x2 = self.current_crop[1].x, self.current_crop[2].x
- local y1, y2 = self.current_crop[1].y, self.current_crop[2].y
+ if self.current_crop ~= nil then
+ local x1, x2 = self.current_crop[1].x, self.current_crop[2].x
+ local y1, y2 = self.current_crop[1].y, self.current_crop[2].y
- self.current_crop.x, self.current_crop.y = x1, y1
- self.current_crop.w, self.current_crop.h = x2 - x1, y2 - y1
+ self.current_crop.x, self.current_crop.y = x1, y1
+ self.current_crop.w, self.current_crop.h = x2 - x1, y2 - y1
- if self.options.even_dimensions then
- self.current_crop.w = self.current_crop.w - (self.current_crop.w % 2)
- self.current_crop.h = self.current_crop.h - (self.current_crop.h % 2)
- end
+ if self.options.even_dimensions then
+ self.current_crop.w = self.current_crop.w - (self.current_crop.w % 2)
+ self.current_crop.h = self.current_crop.h - (self.current_crop.h % 2)
+ end
- self.current_crop.x1, self.current_crop.x2 = x1, x1 + self.current_crop.w
- self.current_crop.y1, self.current_crop.y2 = y1, y1 + self.current_crop.h
+ self.current_crop.x1, self.current_crop.x2 = x1, x1 + self.current_crop.w
+ self.current_crop.y1, self.current_crop.y2 = y1, y1 + self.current_crop.h
- self.current_crop[2].x, self.current_crop[2].y = self.current_crop.x2, self.current_crop.y2
- end
+ self.current_crop[2].x, self.current_crop[2].y = self.current_crop.x2, self.current_crop.y2
+ end
end
-
function ASSCropper:key_event(name)
- if name == "ENTER" then
- self:stop_crop(false)
-
- self:finalize_crop()
-
- if self.callback_on_crop == nil then
- mp.set_osd_ass(0,0, "")
- else
- self.callback_on_crop(self.current_crop)
- end
-
- elseif name == "ESC" then
- self:stop_crop(true)
-
- if self.callback_on_cancel == nil then
- mp.set_osd_ass(0,0, "")
- else
- self.callback_on_cancel()
- end
-
- elseif name == "TEST" then
- self:toggle_testing()
-
- elseif not self.testing_crop then
- if name == "CROP_DETECT" then
- self:toggle_crop_detect()
-
- elseif name == "CROSSHAIR" then
- self.options.draw_mouse = not self.options.draw_mouse;
- elseif name == "INVERT" then
- self.options.color_invert = not self.options.color_invert;
- elseif name == "GUIDES" then
- self.options.guide_type = (self.options.guide_type + 1) % (self.guide_type_count)
- mp.osd_message(self.guide_type_names[self.options.guide_type])
- elseif self.nudges[name] then
- self:nudge(true, unpack(self.nudges[name]))
- elseif self.resizes[name] then
- self:nudge(false, unpack(self.resizes[name]))
- end
- end
+ if name == "ENTER" then
+ self:stop_crop(false)
+
+ self:finalize_crop()
+
+ if self.callback_on_crop == nil then
+ mp.set_osd_ass(0, 0, "")
+ else
+ self.callback_on_crop(self.current_crop)
+ end
+ elseif name == "ESC" then
+ self:stop_crop(true)
+
+ if self.callback_on_cancel == nil then
+ mp.set_osd_ass(0, 0, "")
+ else
+ self.callback_on_cancel()
+ end
+ elseif name == "TEST" then
+ self:toggle_testing()
+ elseif not self.testing_crop then
+ if name == "CROP_DETECT" then
+ self:toggle_crop_detect()
+ elseif name == "CROSSHAIR" then
+ self.options.draw_mouse = not self.options.draw_mouse
+ elseif name == "INVERT" then
+ self.options.color_invert = not self.options.color_invert
+ elseif name == "GUIDES" then
+ self.options.guide_type = (self.options.guide_type + 1) % self.guide_type_count
+ mp.osd_message(self.guide_type_names[self.options.guide_type])
+ elseif self.nudges[name] then
+ self:nudge(true, unpack(self.nudges[name]))
+ elseif self.resizes[name] then
+ self:nudge(false, unpack(self.resizes[name]))
+ end
+ end
end
function ASSCropper:nudge(keep_size, left, top, right, bottom)
- if self.current_crop == nil then return end
-
- local x1, y1 = self.current_crop[1].x, self.current_crop[1].y
- local x2, y2 = self.current_crop[2].x, self.current_crop[2].y
+ if self.current_crop == nil then
+ return
+ end
- local w, h = x2 - x1, y2 - y1
- if not keep_size then
- w, h = 0, 0
+ local x1, y1 = self.current_crop[1].x, self.current_crop[1].y
+ local x2, y2 = self.current_crop[2].x, self.current_crop[2].y
- if self.options.even_dimensions then
- left = left * 2
- top = top * 2
- right = right * 2
- bottom = bottom * 2
- end
+ local w, h = x2 - x1, y2 - y1
+ if not keep_size then
+ w, h = 0, 0
- end
+ if self.options.even_dimensions then
+ left = left * 2
+ top = top * 2
+ right = right * 2
+ bottom = bottom * 2
+ end
+ end
- local vw, vh = self.display_state.video.width, self.display_state.video.height
+ local vw, vh = self.display_state.video.width, self.display_state.video.height
- x1 = math.max(0, math.min(vw-w, x1 + left))
- y1 = math.max(0, math.min(vh-h, y1 + top))
+ x1 = math.max(0, math.min(vw - w, x1 + left))
+ y1 = math.max(0, math.min(vh - h, y1 + top))
- x2 = math.max(w, math.min(vw, x2 + right))
- y2 = math.max(h, math.min(vh, y2 + bottom))
+ x2 = math.max(w, math.min(vw, x2 + right))
+ y2 = math.max(h, math.min(vh, y2 + bottom))
- local x_offset = math.max(0, 0-x1) - math.max(0, x2-vw)
- local y_offset = math.max(0, 0-y1) - math.max(0, y2-vh)
+ local x_offset = math.max(0, 0 - x1) - math.max(0, x2 - vw)
+ local y_offset = math.max(0, 0 - y1) - math.max(0, y2 - vh)
- x1 = x1 + x_offset
- y1 = y1 + y_offset
- x2 = x2 + x_offset
- y2 = y2 + y_offset
+ x1 = x1 + x_offset
+ y1 = y1 + y_offset
+ x2 = x2 + x_offset
+ y2 = y2 + y_offset
- self.current_crop[1].x, self.current_crop[2].x = order_pair(x1, x2)
- self.current_crop[1].y, self.current_crop[2].y = order_pair(y1, y2)
+ self.current_crop[1].x, self.current_crop[2].x = order_pair(x1, x2)
+ self.current_crop[1].y, self.current_crop[2].y = order_pair(y1, y2)
end
function ASSCropper:blackframe_stop()
- if self.detecting_blackframe then
- self.detecting_blackframe:stop()
- self.detecting_blackframe = nil
-
- local filters = mp.get_property_native("vf")
- for i, filter in ipairs(filters) do
- if filter.label == self.blackframe_label then
- table.remove(filters, i)
- end
- end
- mp.set_property_native("vf", filters)
- end
+ if self.detecting_blackframe then
+ self.detecting_blackframe:stop()
+ self.detecting_blackframe = nil
+ local filters = mp.get_property_native("vf")
+ for i, filter in ipairs(filters) do
+ if filter.label == self.blackframe_label then
+ table.remove(filters, i)
+ end
+ end
+ mp.set_property_native("vf", filters)
+ end
end
function ASSCropper:toggle_testing()
- if self.testing_crop then
- self:stop_testing()
- else
- self:start_testing()
- end
+ if self.testing_crop then
+ self:stop_testing()
+ else
+ self:start_testing()
+ end
end
function ASSCropper:start_testing()
- if not self.testing_crop then
-
- local cw = self.current_crop and (self.current_crop[2].x - self.current_crop[1].x) or 0
- local ch = self.current_crop and (self.current_crop[2].y - self.current_crop[1].y) or 0
-
- if cw == 0 or ch == 0 then
- return mp.osd_message("Can't test current crop")
- end
-
- self:cropdetect_stop()
- self:blackframe_stop()
-
- local crop_filter = ('@%s:crop=w=%d:h=%d:x=%d:y=%d'):format(
- self.crop_label, cw, ch, self.current_crop[1].x, self.current_crop[1].y
- )
- local ret = mp.commandv('vf', 'add', crop_filter)
- if ret then
- self.testing_crop = true
- end
- end
+ if not self.testing_crop then
+ local cw = self.current_crop and (self.current_crop[2].x - self.current_crop[1].x) or 0
+ local ch = self.current_crop and (self.current_crop[2].y - self.current_crop[1].y) or 0
+
+ if cw == 0 or ch == 0 then
+ return mp.osd_message("Can't test current crop")
+ end
+
+ self:cropdetect_stop()
+ self:blackframe_stop()
+
+ local crop_filter = ("@%s:crop=w=%d:h=%d:x=%d:y=%d"):format(
+ self.crop_label,
+ cw,
+ ch,
+ self.current_crop[1].x,
+ self.current_crop[1].y
+ )
+ local ret = mp.commandv("vf", "add", crop_filter)
+ if ret then
+ self.testing_crop = true
+ end
+ end
end
function ASSCropper:stop_testing()
- if self.testing_crop then
- local filters = mp.get_property_native("vf")
- for i, filter in ipairs(filters) do
- if filter.label == self.crop_label then
- table.remove(filters, i)
- end
- end
- mp.set_property_native("vf", filters)
- self.testing_crop = false
- end
+ if self.testing_crop then
+ local filters = mp.get_property_native("vf")
+ for i, filter in ipairs(filters) do
+ if filter.label == self.crop_label then
+ table.remove(filters, i)
+ end
+ end
+ mp.set_property_native("vf", filters)
+ self.testing_crop = false
+ end
end
-
function ASSCropper:blackframe_check()
- local blackframe_metadata = mp.get_property_native("vf-metadata/" .. self.blackframe_label)
- local black_percentage = tonumber(blackframe_metadata["lavfi.blackframe.pblack"])
+ local blackframe_metadata = mp.get_property_native("vf-metadata/" .. self.blackframe_label)
+ local black_percentage = tonumber(blackframe_metadata["lavfi.blackframe.pblack"])
- local now = mp.get_time()
- if black_percentage ~= nil and now >= self.blackframe_wait then
- self:blackframe_stop()
+ local now = mp.get_time()
+ if black_percentage ~= nil and now >= self.blackframe_wait then
+ self:blackframe_stop()
- self.options.color_invert = black_percentage < 50
- elseif now > self.blackframe_timeout then
- -- Couldn't get blackframe metadata in time!
- self:blackframe_stop()
- end
+ self.options.color_invert = black_percentage < 50
+ elseif now > self.blackframe_timeout then
+ -- Couldn't get blackframe metadata in time!
+ self:blackframe_stop()
+ end
end
function ASSCropper:blackframe_start()
- self:blackframe_stop()
- if not self.detecting_blackframe then
+ self:blackframe_stop()
+ if not self.detecting_blackframe then
+ local blackframe_filter = ("@%s:blackframe=amount=%d:threshold=%d"):format(self.blackframe_label, 0, 128)
- local blackframe_filter = ('@%s:blackframe=amount=%d:threshold=%d'):format(self.blackframe_label, 0, 128)
+ local ret = mp.commandv("vf", "add", blackframe_filter)
+ if ret then
+ self.blackframe_wait = mp.get_time() + 0.15
+ self.blackframe_timeout = self.blackframe_wait + 1
- local ret = mp.commandv('vf', 'add', blackframe_filter)
- if ret then
- self.blackframe_wait = mp.get_time() + 0.15
- self.blackframe_timeout = self.blackframe_wait + 1
-
- self.detecting_blackframe = mp.add_periodic_timer(1/10, function()
- self:blackframe_check()
- end)
- end
- end
+ self.detecting_blackframe = mp.add_periodic_timer(1 / 10, function()
+ self:blackframe_check()
+ end)
+ end
+ end
end
function ASSCropper:cropdetect_stop()
- if self.detecting_crop then
- self.detecting_crop:stop()
- self.detecting_crop = nil
- self.cropdetect_wait = nil
- self.cropdetect_timeout = nil
-
- local filters = mp.get_property_native("vf")
- for i, filter in ipairs(filters) do
- if filter.label == self.cropdetect_label then
- table.remove(filters, i)
- end
- end
- mp.set_property_native("vf", filters)
- end
-
+ if self.detecting_crop then
+ self.detecting_crop:stop()
+ self.detecting_crop = nil
+ self.cropdetect_wait = nil
+ self.cropdetect_timeout = nil
+
+ local filters = mp.get_property_native("vf")
+ for i, filter in ipairs(filters) do
+ if filter.label == self.cropdetect_label then
+ table.remove(filters, i)
+ end
+ end
+ mp.set_property_native("vf", filters)
+ end
end
function ASSCropper:cropdetect_check()
- local cropdetect_metadata = mp.get_property_native("vf-metadata/" .. self.cropdetect_label)
- local get_n = function(s) return tonumber(cropdetect_metadata["lavfi.cropdetect." .. s]) end
+ local cropdetect_metadata = mp.get_property_native("vf-metadata/" .. self.cropdetect_label)
+ local get_n = function(s)
+ return tonumber(cropdetect_metadata["lavfi.cropdetect." .. s])
+ end
- local now = mp.get_time()
- if not isempty(cropdetect_metadata) and now >= self.cropdetect_wait then
- self:cropdetect_stop()
+ local now = mp.get_time()
+ if not isempty(cropdetect_metadata) and now >= self.cropdetect_wait then
+ self:cropdetect_stop()
- self.current_crop = {
- {x=get_n("x1"), y=get_n("y1")},
- {x=get_n("x2")+1, y=get_n("y2")+1},
- }
+ self.current_crop = {
+ { x = get_n("x1"), y = get_n("y1") },
+ { x = get_n("x2") + 1, y = get_n("y2") + 1 },
+ }
- mp.osd_message("Crop detected")
- elseif now > self.cropdetect_timeout then
- mp.osd_message("Crop detect timed out")
- self:cropdetect_stop()
- end
+ mp.osd_message("Crop detected")
+ elseif now > self.cropdetect_timeout then
+ mp.osd_message("Crop detect timed out")
+ self:cropdetect_stop()
+ end
end
function ASSCropper:toggle_crop_detect()
- if self.detecting_crop then
- self:cropdetect_stop()
- mp.osd_message("Cancelled crop detect")
-
- else
- local cropdetect_filter = ('@%s:cropdetect=limit=%f:round=2:reset=0'):format(self.cropdetect_label, 30/255)
-
- local ret = mp.commandv('vf', 'add', cropdetect_filter)
- if not ret then
- mp.osd_message("Crop detect failed")
- else
- self.cropdetect_wait = mp.get_time() + 0.2
- self.cropdetect_timeout = self.cropdetect_wait + 1.5
-
- mp.osd_message("Starting automatic crop detect")
- self.detecting_crop = mp.add_periodic_timer(1/10, function()
- self:cropdetect_check()
- end)
- end
- end
+ if self.detecting_crop then
+ self:cropdetect_stop()
+ mp.osd_message("Cancelled crop detect")
+ else
+ local cropdetect_filter = ("@%s:cropdetect=limit=%f:round=2:reset=0"):format(self.cropdetect_label, 30 / 255)
+
+ local ret = mp.commandv("vf", "add", cropdetect_filter)
+ if not ret then
+ mp.osd_message("Crop detect failed")
+ else
+ self.cropdetect_wait = mp.get_time() + 0.2
+ self.cropdetect_timeout = self.cropdetect_wait + 1.5
+
+ mp.osd_message("Starting automatic crop detect")
+ self.detecting_crop = mp.add_periodic_timer(1 / 10, function()
+ self:cropdetect_check()
+ end)
+ end
+ end
end
-
function ASSCropper:start_crop(options, on_crop, on_cancel)
- -- Refresh display state
- self.display_state:recalculate_bounds(true)
- if self.display_state.video_ready then
- self.active = true
- self.tick_timer:resume()
+ -- Refresh display state
+ self.display_state:recalculate_bounds(true)
+ if self.display_state.video_ready then
+ self.active = true
+ self.tick_timer:resume()
- self.options = {}
+ self.options = {}
- for k, v in pairs(self.default_options) do
- self.options[k] = v
- end
- for k, v in pairs(options or {}) do
- self.options[k] = v
- end
+ for k, v in pairs(self.default_options) do
+ self.options[k] = v
+ end
+ for k, v in pairs(options or {}) do
+ self.options[k] = v
+ end
- self.callback_on_crop = on_crop
- self.callback_on_cancel = on_cancel
+ self.callback_on_crop = on_crop
+ self.callback_on_cancel = on_cancel
- self.dragging = 0
+ self.dragging = 0
- self:enable_key_bindings()
- self:update_mouse_position()
+ self:enable_key_bindings()
+ self:update_mouse_position()
- if self.options.auto_invert then
- self:blackframe_start()
- end
- end
+ if self.options.auto_invert then
+ self:blackframe_start()
+ end
+ end
end
function ASSCropper:stop_crop(clear)
- self.active = false
- self.tick_timer:stop()
+ self.active = false
+ self.tick_timer:stop()
- self:cropdetect_stop()
- self:blackframe_stop()
- self:stop_testing()
+ self:cropdetect_stop()
+ self:blackframe_stop()
+ self:stop_testing()
- self:disable_key_bindings()
- if clear then
- self.current_crop = nil
- end
+ self:disable_key_bindings()
+ if clear then
+ self.current_crop = nil
+ end
end
-
function ASSCropper:on_tick()
- -- Unused, for debugging
- if self.active then
- self.display_state:recalculate_bounds()
- self:render()
- end
+ -- Unused, for debugging
+ if self.active then
+ self.display_state:recalculate_bounds()
+ self:render()
+ end
end
-
function ASSCropper:update_mouse_position()
- -- These are real on-screen coords.
- self.mouse_screen.x, self.mouse_screen.y = mp.get_mouse_pos()
-
- if self.display_state:recalculate_bounds() and self.display_state.video_ready then
- -- These are on-video coords.
- local mx, my = self.display_state:screen_to_video(self.mouse_screen.x, self.mouse_screen.y)
- self.mouse_video.x = mx
- self.mouse_video.y = my
- end
+ -- These are real on-screen coords.
+ self.mouse_screen.x, self.mouse_screen.y = mp.get_mouse_pos()
+ if self.display_state:recalculate_bounds() and self.display_state.video_ready then
+ -- These are on-video coords.
+ local mx, my = self.display_state:screen_to_video(self.mouse_screen.x, self.mouse_screen.y)
+ self.mouse_video.x = mx
+ self.mouse_video.y = my
+ end
end
-
function ASSCropper:get_hitboxes(crop_box)
- crop_box = crop_box or self.current_crop
- if crop_box == nil then
- return nil
- end
-
- local x1, x2 = order_pair(crop_box[1].x, crop_box[2].x)
- local y1, y2 = order_pair(crop_box[1].y, crop_box[2].y)
- local w, h = math.abs(x2 - x1), math.abs(y2 - y1)
-
- -- Corner and required corner size in videospace pixels
- local mult = math.min(self.display_state.scale.x, self.display_state.scale.y)
- local videospace_corner_size = self.corner_size * mult
- local videospace_required_size = self.corner_required_size * mult
-
- local handles_outside = (math.min(w, h) <= videospace_required_size)
-
- local hitbox_bases = {
- { x1, y2, x1, y2 }, -- BL
- { x1, y2, x2, y2 }, -- B
- { x2, y2, x2, y2 }, -- BR
-
- { x1, y1, x1, y2 }, -- L
- { x1, y1, x2, y2 }, -- Center
- { x2, y1, x2, y2 }, -- R
-
- { x1, y1, x1, y1 }, -- TL
- { x1, y1, x2, y1 }, -- T
- { x2, y1, x2, y1 } -- TR
- }
-
- local hitbox_mults
- if handles_outside then
- hitbox_mults = {
- {-1, 0, 0, 1},
- { 0, 0, 0, 1},
- { 0, 0, 1, 1},
-
- {-1, 0, 0, 0},
- { 0, 0, 0, 0},
- { 0, 0, 1, 0},
-
- {-1, -1, 0, 0},
- { 0, -1, 0, 0},
- { 0, -1, 1, 0}
- }
-
- else
- hitbox_mults = {
- { 0, -1, 1, 0},
- { 1, -1, -1, 0},
- {-1, -1, 0, 0},
-
- { 0, 1, 1, -1},
- { 1, 1, -1, -1},
- {-1, 1, 0, -1},
-
- { 0, 0, 1, 1},
- { 1, 0, -1, 1},
- {-1, 0, 0, 1}
- }
- end
-
-
- local hitboxes = {}
- for index, hitbox_base in ipairs(hitbox_bases) do
- local hitbox_mult = hitbox_mults[index]
-
- hitboxes[index] = {
- hitbox_base[1] + hitbox_mult[1] * videospace_corner_size,
- hitbox_base[2] + hitbox_mult[2] * videospace_corner_size,
- hitbox_base[3] + hitbox_mult[3] * videospace_corner_size,
- hitbox_base[4] + hitbox_mult[4] * videospace_corner_size
- }
- end
- -- Pseudobox to easily pass the original crop box
- hitboxes[10] = {x1, y1, x2, y2}
-
- return hitboxes
+ crop_box = crop_box or self.current_crop
+ if crop_box == nil then
+ return nil
+ end
+
+ local x1, x2 = order_pair(crop_box[1].x, crop_box[2].x)
+ local y1, y2 = order_pair(crop_box[1].y, crop_box[2].y)
+ local w, h = math.abs(x2 - x1), math.abs(y2 - y1)
+
+ -- Corner and required corner size in videospace pixels
+ local mult = math.min(self.display_state.scale.x, self.display_state.scale.y)
+ local videospace_corner_size = self.corner_size * mult
+ local videospace_required_size = self.corner_required_size * mult
+
+ local handles_outside = (math.min(w, h) <= videospace_required_size)
+
+ local hitbox_bases = {
+ { x1, y2, x1, y2 }, -- BL
+ { x1, y2, x2, y2 }, -- B
+ { x2, y2, x2, y2 }, -- BR
+
+ { x1, y1, x1, y2 }, -- L
+ { x1, y1, x2, y2 }, -- Center
+ { x2, y1, x2, y2 }, -- R
+
+ { x1, y1, x1, y1 }, -- TL
+ { x1, y1, x2, y1 }, -- T
+ { x2, y1, x2, y1 }, -- TR
+ }
+
+ local hitbox_mults
+ if handles_outside then
+ hitbox_mults = {
+ { -1, 0, 0, 1 },
+ { 0, 0, 0, 1 },
+ { 0, 0, 1, 1 },
+
+ { -1, 0, 0, 0 },
+ { 0, 0, 0, 0 },
+ { 0, 0, 1, 0 },
+
+ { -1, -1, 0, 0 },
+ { 0, -1, 0, 0 },
+ { 0, -1, 1, 0 },
+ }
+ else
+ hitbox_mults = {
+ { 0, -1, 1, 0 },
+ { 1, -1, -1, 0 },
+ { -1, -1, 0, 0 },
+
+ { 0, 1, 1, -1 },
+ { 1, 1, -1, -1 },
+ { -1, 1, 0, -1 },
+
+ { 0, 0, 1, 1 },
+ { 1, 0, -1, 1 },
+ { -1, 0, 0, 1 },
+ }
+ end
+
+ local hitboxes = {}
+ for index, hitbox_base in ipairs(hitbox_bases) do
+ local hitbox_mult = hitbox_mults[index]
+
+ hitboxes[index] = {
+ hitbox_base[1] + hitbox_mult[1] * videospace_corner_size,
+ hitbox_base[2] + hitbox_mult[2] * videospace_corner_size,
+ hitbox_base[3] + hitbox_mult[3] * videospace_corner_size,
+ hitbox_base[4] + hitbox_mult[4] * videospace_corner_size,
+ }
+ end
+ -- Pseudobox to easily pass the original crop box
+ hitboxes[10] = { x1, y1, x2, y2 }
+
+ return hitboxes
end
-
function ASSCropper:hit_test(hitboxes, position)
- if hitboxes == nil then
- return 0
-
- else
- local px, py = position.x, position.y
+ if hitboxes == nil then
+ return 0
+ else
+ local px, py = position.x, position.y
- for i = 1,9 do
- local hb = hitboxes[i]
+ for i = 1, 9 do
+ local hb = hitboxes[i]
- if (px >= hb[1] and px < hb[3]) and (py >= hb[2] and py < hb[4]) then
- return i
- end
-
- end
- -- No hits
- return 0
- end
+ if (px >= hb[1] and px < hb[3]) and (py >= hb[2] and py < hb[4]) then
+ return i
+ end
+ end
+ -- No hits
+ return 0
+ end
end
-
function ASSCropper:on_mouse(button, event, shift_down)
- if not(event.event == "up" or event.event == "down") then return end
- mouse_down = event.event == "down"
- shift_down = shift_down or false
-
- if button == "mouse_btn0" and self.active and not self.detecting_crop and not self.testing_crop then
-
- local mouse_pos = {x=self.mouse_video.x, y=self.mouse_video.y}
-
- -- Helpers
- local xy_same = function(a, b) return a.x == b.x and a.y == b.y end
- local xy_distance = function(a, b)
- local dx = a.x - b.x
- local dy = a.y - b.y
- return math.sqrt( dx*dx + dy*dy )
- end
- --
-
- if mouse_down then -- Mouse pressed
-
- local bound_mouse_pos = {
- x = math.max(0, math.min(self.display_state.video.width, mouse_pos.x)),
- y = math.max(0, math.min(self.display_state.video.height, mouse_pos.y)),
- }
-
- if self.current_crop == nil then
- self.current_crop = { bound_mouse_pos, bound_mouse_pos }
-
- self.dragging = 3
- self.anchor_pos = {bound_mouse_pos.x, bound_mouse_pos.y}
-
- self.crop_ratio = 1
- self.drag_start = bound_mouse_pos
-
- local handle_pos = self:_get_anchor_positions()[hit]
- self.drag_offset = {0, 0}
-
- self.restrict_ratio = shift_down
-
- elseif self.dragging == 0 then
- -- Check if we drag from a handle
- local hitboxes = self:get_hitboxes()
- local hit = self:hit_test(hitboxes, mouse_pos)
-
- self.dragging = hit
- self.anchor_pos = self:_get_anchor_positions()[10 - hit]
-
- self.crop_ratio = (hitboxes[10][3] - hitboxes[10][1]) / (hitboxes[10][4] - hitboxes[10][2])
- self.drag_start = mouse_pos
-
- local handle_pos = self:_get_anchor_positions()[hit] or {mouse_pos.x, mouse_pos.y}
- self.drag_offset = { mouse_pos.x - handle_pos[1], mouse_pos.y - handle_pos[2]}
-
- self.restrict_ratio = shift_down
-
- -- Start a new drag if not on handle
- if self.dragging == 0 then
- self.current_crop = { bound_mouse_pos, bound_mouse_pos }
- self.crop_ratio = 1
-
- self.dragging = 3
- self.anchor_pos = {bound_mouse_pos.x, bound_mouse_pos.y}
- -- self.drag_start = mouse_pos
- end
- end
-
- else -- Mouse released
-
- if xy_same(self.current_crop[1], self.current_crop[2]) and xy_distance(self.current_crop[1], mouse_pos) < 5 then
- -- Mouse released after first click - ignore
-
- elseif self.dragging > 0 then
- -- Adjust current crop
- self.current_crop = self:offset_crop_by_drag()
- self.dragging = 0
- end
- end
-
- end
+ if not (event.event == "up" or event.event == "down") then
+ return
+ end
+ mouse_down = event.event == "down"
+ shift_down = shift_down or false
+
+ if button == "mouse_btn0" and self.active and not self.detecting_crop and not self.testing_crop then
+ local mouse_pos = { x = self.mouse_video.x, y = self.mouse_video.y }
+
+ -- Helpers
+ local xy_same = function(a, b)
+ return a.x == b.x and a.y == b.y
+ end
+ local xy_distance = function(a, b)
+ local dx = a.x - b.x
+ local dy = a.y - b.y
+ return math.sqrt(dx * dx + dy * dy)
+ end
+ --
+
+ if mouse_down then -- Mouse pressed
+ local bound_mouse_pos = {
+ x = math.max(0, math.min(self.display_state.video.width, mouse_pos.x)),
+ y = math.max(0, math.min(self.display_state.video.height, mouse_pos.y)),
+ }
+
+ if self.current_crop == nil then
+ self.current_crop = { bound_mouse_pos, bound_mouse_pos }
+
+ self.dragging = 3
+ self.anchor_pos = { bound_mouse_pos.x, bound_mouse_pos.y }
+
+ self.crop_ratio = 1
+ self.drag_start = bound_mouse_pos
+
+ local handle_pos = self:_get_anchor_positions()[hit]
+ self.drag_offset = { 0, 0 }
+
+ self.restrict_ratio = shift_down
+ elseif self.dragging == 0 then
+ -- Check if we drag from a handle
+ local hitboxes = self:get_hitboxes()
+ local hit = self:hit_test(hitboxes, mouse_pos)
+
+ self.dragging = hit
+ self.anchor_pos = self:_get_anchor_positions()[10 - hit]
+
+ self.crop_ratio = (hitboxes[10][3] - hitboxes[10][1]) / (hitboxes[10][4] - hitboxes[10][2])
+ self.drag_start = mouse_pos
+
+ local handle_pos = self:_get_anchor_positions()[hit] or { mouse_pos.x, mouse_pos.y }
+ self.drag_offset = { mouse_pos.x - handle_pos[1], mouse_pos.y - handle_pos[2] }
+
+ self.restrict_ratio = shift_down
+
+ -- Start a new drag if not on handle
+ if self.dragging == 0 then
+ self.current_crop = { bound_mouse_pos, bound_mouse_pos }
+ self.crop_ratio = 1
+
+ self.dragging = 3
+ self.anchor_pos = { bound_mouse_pos.x, bound_mouse_pos.y }
+ -- self.drag_start = mouse_pos
+ end
+ end
+ else -- Mouse released
+ if
+ xy_same(self.current_crop[1], self.current_crop[2])
+ and xy_distance(self.current_crop[1], mouse_pos) < 5
+ then
+ -- Mouse released after first click - ignore
+ elseif self.dragging > 0 then
+ -- Adjust current crop
+ self.current_crop = self:offset_crop_by_drag()
+ self.dragging = 0
+ end
+ end
+ end
end
-
function ASSCropper:_get_anchor_positions()
- local x1, y1 = self.current_crop[1].x, self.current_crop[1].y
- local x2, y2 = self.current_crop[2].x, self.current_crop[2].y
- return {
- [1] = {x1, y2},
- [2] = {(x1+x2)/2, y2},
- [3] = {x2, y2},
+ local x1, y1 = self.current_crop[1].x, self.current_crop[1].y
+ local x2, y2 = self.current_crop[2].x, self.current_crop[2].y
+ return {
+ [1] = { x1, y2 },
+ [2] = { (x1 + x2) / 2, y2 },
+ [3] = { x2, y2 },
- [4] = {x1, (y1+y2)/2},
- [5] = {(x1+x2)/2, (y1+y2)/2},
- [6] = {x2, (y1+y2)/2},
+ [4] = { x1, (y1 + y2) / 2 },
+ [5] = { (x1 + x2) / 2, (y1 + y2) / 2 },
+ [6] = { x2, (y1 + y2) / 2 },
- [7] = {x1, y1},
- [8] = {(x1+x2)/2, y1},
- [9] = {x2, y1},
- }
+ [7] = { x1, y1 },
+ [8] = { (x1 + x2) / 2, y1 },
+ [9] = { x2, y1 },
+ }
end
-
function ASSCropper:offset_crop_by_drag()
- -- Here be dragons lol
- local vw, vh = self.display_state.video.width, self.display_state.video.height
- local mx, my = self.mouse_video.x, self.mouse_video.y
-
- local x1, x2 = self.current_crop[1].x, self.current_crop[2].x
- local y1, y2 = self.current_crop[1].y, self.current_crop[2].y
-
- local anchor_positions = self:_get_anchor_positions()
-
- local handle = self.dragging
- if handle > 0 then
- local ax, ay = self.anchor_pos[1], self.anchor_pos[2]
-
- local ox, oy = self.drag_offset[1], self.drag_offset[2]
-
- local dx, dy = mx - ax - ox, my - ay - oy
-
- -- Select active corner
- if handle % 2 == 1 and handle ~= 5 then -- Change corners 4/6, 2/8
- handle = (mx - ox < ax) and 1 or 3
- handle = handle + ( (my - oy < ay) and 6 or 0)
- else -- Change edges 1, 3, 7, 9
- if handle == 4 and mx - ox > ax then
- handle = 6
- elseif handle == 6 and mx - ox < ax then
- handle = 4
- elseif handle == 2 and my - oy < ay then
- handle = 8
- elseif handle == 8 and my - oy > ay then
- handle = 2
- end
- end
-
- -- Handle booleans for logic
- local h_bot = handle >= 1 and handle <= 3
- local h_top = handle >= 7 and handle <= 9
- local h_left = (handle - 1) % 3 == 0
- local h_right = handle % 3 == 0
-
- local h_horiz = handle == 4 or handle == 6
- local h_vert = handle == 2 or handle == 8
-
- -- Keep rect aspect ratio
- if self.restrict_ratio then
- local adx, ady = math.abs(dx), math.abs(dy)
-
- -- Fit rect to mouse
- local tmpy = adx / self.crop_ratio
- if tmpy < ady then
- adx = ady * self.crop_ratio
- else
- ady = tmpy
- end
-
- -- Figure out max size for corners, limit adx/ady
- local max_w, max_h = vw, vh
-
- if h_bot then
- max_h = vh - ay -- Max height is from anchor to video bottom
- elseif h_top then
- max_h = ay -- Max height is from video bottom to anchor
- elseif h_horiz then
- -- Max height is closest edge * 2
- max_h = math.min(vh - ay, ay) * 2
- end
-
- if h_left then
- max_w = ax
- elseif h_right then
- max_w = vw - ax
- elseif h_vert then
- max_w = math.min(vw - ax, ax) * 2
- end
-
- -- Limit size to corners
- if handle ~= 5 then
- -- TODO this can be done tidier?
-
- -- If wider than max width, scale down
- if adx > max_w then
- adx = max_w
- ady = adx / self.crop_ratio
- end
- -- If taller than max height, scale down
- if ady > max_h then
- ady = max_h
- adx = ady * self.crop_ratio
- end
- end
-
- -- Hacky offsets
- if handle == 1 then
- dx = -adx
- dy = ady
- elseif handle == 2 then
- dx = adx
- dy = ady
- elseif handle == 3 then
- dx = adx
- dy = ady
-
- elseif handle == 4 then
- dx = -adx
- dy = ady
- elseif handle == 5 then
- -- pass
- elseif handle == 6 then
- dx = adx
- dy = ady
-
- elseif handle == 7 then
- dy = -ady
- dx = -adx
- elseif handle == 8 then
- dx = adx
- dy = -ady
- elseif handle == 9 then
- dx = adx
- dy = -ady
- end
- end
-
- -- Can this be done not-manually?
- -- Re-create the rect with some corners anchored etc
- if handle == 5 then
- -- Simply move the box around
- x1, x2 = x1+dx, x2+dx
- y1, y2 = y1+dy, y2+dy
-
- elseif handle == 1 then
- x1, x2 = ax + dx, ax
- y1, y2 = ay, ay+dy
-
- elseif handle == 2 then
- y1, y2 = ay, ay + dy
-
- if self.restrict_ratio then
- x1, x2 = ax - dx/2, ax + dx/2
- end
-
- elseif handle == 3 then
- x1, x2 = ax, ax + dx
- y1, y2 = ay, ay + dy
-
- elseif handle == 4 then
- x1, x2 = ax + dx, ax
-
- if self.restrict_ratio then
- y1, y2 = ay - dy/2, ay + dy/2
- end
-
- elseif handle == 6 then
- x1, x2 = ax, ax + dx
-
- if self.restrict_ratio then
- y1, y2 = ay - dy/2, ay + dy/2
- end
-
-
- elseif handle == 7 then
- x1, x2 = ax + dx, ax
- y1, y2 = ay + dy, ay
-
- elseif handle == 8 then
- y1, y2 = ay + dy, ay
-
- if self.restrict_ratio then
- x1, x2 = ax - dx/2, ax + dx/2
- end
-
- elseif handle == 9 then
- x1, x2 = ax, ax + dx
- y1, y2 = ay + dy, ay
- end
-
-
- if self.dragging == 5 then
- -- On moving the entire box, we have to figure out how much to "offset" every corner if we go over the edge
- local x_min = math.max(0, 0-x1)
- local y_min = math.max(0, 0-y1)
-
- local x_max = math.max(0, x2-vw)
- local y_max = math.max(0, y2-vh)
-
- x1 = x1 + x_min - x_max
- y1 = y1 + y_min - y_max
- x2 = x2 + x_min - x_max
- y2 = y2 + y_min - y_max
- elseif not self.restrict_ratio then
- -- This is already done for restricted ratios, hence the if
-
- -- Constrict the crop to video space
- -- Since one corner/edge is moved at a time, we can just minmax this
- x1, x2 = math.max(0, x1), math.min(vw, x2)
- y1, y2 = math.max(0, y1), math.min(vh, y2)
- end
- end -- /drag
-
- if self.dragging > 0 and self.options.even_dimensions then
- local w, h = x2 - x1, y2 - y1
- local even_w = w - (w % 2)
- local even_h = h - (h % 2)
-
- if handle == 1 or handle == 2 or handle == 3 then
- y2 = y1 + even_h
- elseif handle == 7 or handle == 8 or handle == 9 then
- y1 = y2 - even_h
- end
- if handle == 1 or handle == 4 or handle == 7 then
- x1 = x2 - even_w
- elseif handle == 3 or handle == 6 or handle == 9 then
- x2 = x1 + even_w
- end
- end
-
- local fx1, fx2 = order_pair(math.floor(x1), math.floor(x2))
- local fy1, fy2 = order_pair(math.floor(y1), math.floor(y2))
-
- -- msg.info(fx1, fy1, fx2, fy2, handle)
-
- return { {x=fx1, y=fy1}, {x=fx2, y=fy2} }, handle
-end
-
-
-function order_pair( a, b )
- if a < b then
- return a, b
- else
- return b, a
- end
+ -- Here be dragons lol
+ local vw, vh = self.display_state.video.width, self.display_state.video.height
+ local mx, my = self.mouse_video.x, self.mouse_video.y
+
+ local x1, x2 = self.current_crop[1].x, self.current_crop[2].x
+ local y1, y2 = self.current_crop[1].y, self.current_crop[2].y
+
+ local anchor_positions = self:_get_anchor_positions()
+
+ local handle = self.dragging
+ if handle > 0 then
+ local ax, ay = self.anchor_pos[1], self.anchor_pos[2]
+
+ local ox, oy = self.drag_offset[1], self.drag_offset[2]
+
+ local dx, dy = mx - ax - ox, my - ay - oy
+
+ -- Select active corner
+ if handle % 2 == 1 and handle ~= 5 then -- Change corners 4/6, 2/8
+ handle = (mx - ox < ax) and 1 or 3
+ handle = handle + ((my - oy < ay) and 6 or 0)
+ else -- Change edges 1, 3, 7, 9
+ if handle == 4 and mx - ox > ax then
+ handle = 6
+ elseif handle == 6 and mx - ox < ax then
+ handle = 4
+ elseif handle == 2 and my - oy < ay then
+ handle = 8
+ elseif handle == 8 and my - oy > ay then
+ handle = 2
+ end
+ end
+
+ -- Handle booleans for logic
+ local h_bot = handle >= 1 and handle <= 3
+ local h_top = handle >= 7 and handle <= 9
+ local h_left = (handle - 1) % 3 == 0
+ local h_right = handle % 3 == 0
+
+ local h_horiz = handle == 4 or handle == 6
+ local h_vert = handle == 2 or handle == 8
+
+ -- Keep rect aspect ratio
+ if self.restrict_ratio then
+ local adx, ady = math.abs(dx), math.abs(dy)
+
+ -- Fit rect to mouse
+ local tmpy = adx / self.crop_ratio
+ if tmpy < ady then
+ adx = ady * self.crop_ratio
+ else
+ ady = tmpy
+ end
+
+ -- Figure out max size for corners, limit adx/ady
+ local max_w, max_h = vw, vh
+
+ if h_bot then
+ max_h = vh - ay -- Max height is from anchor to video bottom
+ elseif h_top then
+ max_h = ay -- Max height is from video bottom to anchor
+ elseif h_horiz then
+ -- Max height is closest edge * 2
+ max_h = math.min(vh - ay, ay) * 2
+ end
+
+ if h_left then
+ max_w = ax
+ elseif h_right then
+ max_w = vw - ax
+ elseif h_vert then
+ max_w = math.min(vw - ax, ax) * 2
+ end
+
+ -- Limit size to corners
+ if handle ~= 5 then
+ -- TODO this can be done tidier?
+
+ -- If wider than max width, scale down
+ if adx > max_w then
+ adx = max_w
+ ady = adx / self.crop_ratio
+ end
+ -- If taller than max height, scale down
+ if ady > max_h then
+ ady = max_h
+ adx = ady * self.crop_ratio
+ end
+ end
+
+ -- Hacky offsets
+ if handle == 1 then
+ dx = -adx
+ dy = ady
+ elseif handle == 2 then
+ dx = adx
+ dy = ady
+ elseif handle == 3 then
+ dx = adx
+ dy = ady
+ elseif handle == 4 then
+ dx = -adx
+ dy = ady
+ elseif handle == 5 then
+ -- pass
+ elseif handle == 6 then
+ dx = adx
+ dy = ady
+ elseif handle == 7 then
+ dy = -ady
+ dx = -adx
+ elseif handle == 8 then
+ dx = adx
+ dy = -ady
+ elseif handle == 9 then
+ dx = adx
+ dy = -ady
+ end
+ end
+
+ -- Can this be done not-manually?
+ -- Re-create the rect with some corners anchored etc
+ if handle == 5 then
+ -- Simply move the box around
+ x1, x2 = x1 + dx, x2 + dx
+ y1, y2 = y1 + dy, y2 + dy
+ elseif handle == 1 then
+ x1, x2 = ax + dx, ax
+ y1, y2 = ay, ay + dy
+ elseif handle == 2 then
+ y1, y2 = ay, ay + dy
+
+ if self.restrict_ratio then
+ x1, x2 = ax - dx / 2, ax + dx / 2
+ end
+ elseif handle == 3 then
+ x1, x2 = ax, ax + dx
+ y1, y2 = ay, ay + dy
+ elseif handle == 4 then
+ x1, x2 = ax + dx, ax
+
+ if self.restrict_ratio then
+ y1, y2 = ay - dy / 2, ay + dy / 2
+ end
+ elseif handle == 6 then
+ x1, x2 = ax, ax + dx
+
+ if self.restrict_ratio then
+ y1, y2 = ay - dy / 2, ay + dy / 2
+ end
+ elseif handle == 7 then
+ x1, x2 = ax + dx, ax
+ y1, y2 = ay + dy, ay
+ elseif handle == 8 then
+ y1, y2 = ay + dy, ay
+
+ if self.restrict_ratio then
+ x1, x2 = ax - dx / 2, ax + dx / 2
+ end
+ elseif handle == 9 then
+ x1, x2 = ax, ax + dx
+ y1, y2 = ay + dy, ay
+ end
+
+ if self.dragging == 5 then
+ -- On moving the entire box, we have to figure out how much to "offset" every corner if we go over the edge
+ local x_min = math.max(0, 0 - x1)
+ local y_min = math.max(0, 0 - y1)
+
+ local x_max = math.max(0, x2 - vw)
+ local y_max = math.max(0, y2 - vh)
+
+ x1 = x1 + x_min - x_max
+ y1 = y1 + y_min - y_max
+ x2 = x2 + x_min - x_max
+ y2 = y2 + y_min - y_max
+ elseif not self.restrict_ratio then
+ -- This is already done for restricted ratios, hence the if
+
+ -- Constrict the crop to video space
+ -- Since one corner/edge is moved at a time, we can just minmax this
+ x1, x2 = math.max(0, x1), math.min(vw, x2)
+ y1, y2 = math.max(0, y1), math.min(vh, y2)
+ end
+ end -- /drag
+
+ if self.dragging > 0 and self.options.even_dimensions then
+ local w, h = x2 - x1, y2 - y1
+ local even_w = w - (w % 2)
+ local even_h = h - (h % 2)
+
+ if handle == 1 or handle == 2 or handle == 3 then
+ y2 = y1 + even_h
+ elseif handle == 7 or handle == 8 or handle == 9 then
+ y1 = y2 - even_h
+ end
+ if handle == 1 or handle == 4 or handle == 7 then
+ x1 = x2 - even_w
+ elseif handle == 3 or handle == 6 or handle == 9 then
+ x2 = x1 + even_w
+ end
+ end
+
+ local fx1, fx2 = order_pair(math.floor(x1), math.floor(x2))
+ local fy1, fy2 = order_pair(math.floor(y1), math.floor(y2))
+
+ -- msg.info(fx1, fy1, fx2, fy2, handle)
+
+ return { { x = fx1, y = fy1 }, { x = fx2, y = fy2 } }, handle
+end
+
+function order_pair(a, b)
+ if a < b then
+ return a, b
+ else
+ return b, a
+ end
end
-
function ASSCropper:render()
- -- For debugging
- local ass_txt = self:get_render_ass()
+ -- For debugging
+ local ass_txt = self:get_render_ass()
- local ds = self.display_state
- mp.set_osd_ass(ds.screen.width, ds.screen.height, ass_txt)
+ local ds = self.display_state
+ mp.set_osd_ass(ds.screen.width, ds.screen.height, ass_txt)
end
-
function ASSCropper:get_render_ass(dim_only)
- if not self.display_state.video_ready then
- msg.info("No video info on display_state")
- return ""
- end
-
- line_color = self.options.color_invert and 20 or 220
- local guide_format = string.format("{\\3a&HFF&\\3a&H%02X&\\3c&H%02X%02X%02X&\\bord1\\shad0}", 128, line_color, line_color, line_color)
-
- ass = assdraw.ass_new()
- if self.current_crop then
-
- if self.testing_crop then
- -- Just draw simple help
- ass:new_event()
- ass:pos(self.display_state.screen.width - 5, 5)
- ass:append( string.format("{\\fs%d\\an%d\\bord2}", self.text_size, 9) )
-
- local fmt_key = function( key, text ) return string.format("[{\\c&HBEBEBE&}%s{\\c} %s]", key:upper(), text) end
-
- ass:append(fmt_key("ENTER", "Accept crop") .. " " .. fmt_key("ESC", "Cancel crop") .. '\\N' .. fmt_key("T", "Stop testing"))
- return ass.text
- end
-
- local temp_crop, drawn_handle = self:offset_crop_by_drag()
- local v_hb = self:get_hitboxes(temp_crop)
- -- Map coords to screen
- local s_hb = {}
- for index, coords in pairs(v_hb) do
- local x1, y1 = self.display_state:video_to_screen(coords[1], coords[2])
- local x2, y2 = self.display_state:video_to_screen(coords[3], coords[4])
- s_hb[index] = {x1, y1, x2, y2}
- end
-
- -- Full crop
- local v_crop = v_hb[10] -- Video-space
- local s_crop = s_hb[10] -- Screen-space
-
-
- -- Inverse clipping for the crop box
- ass:new_event()
- ass:append(string.format("{\\iclip(%d,%d,%d,%d)}", s_crop[1], s_crop[2], s_crop[3], s_crop[4]))
-
- -- Dim overlay
- local format_dim = string.format("{\\bord0\\1a&H%02X&\\1c&H%02X%02X%02X&}", self.overlay_transparency, self.overlay_lightness, self.overlay_lightness, self.overlay_lightness)
- ass:pos(0,0)
- ass:draw_start()
- ass:append(format_dim)
- ass:rect_cw(0, 0, self.display_state.screen.width, self.display_state.screen.height)
- ass:draw_stop()
-
- if dim_only then -- Early out with just the dim outline
- return ass.text
- end
-
- if draw_text then
- -- Text on end
- ass:new_event()
- ass:pos(ce_x, ce_y)
- -- Text align
- local txt_a = ((ce_x > cs_x) and 3 or 1) + ((ce_y > cs_y) and 0 or 6)
- ass:an( txt_a )
- ass:append("{\\fs20\\shad0\\be0\\bord2}")
- ass:append(string.format("%dx%d", math.abs(ce_x-cs_x), math.abs(ce_y-cs_y)) )
- end
-
-
- local box_format = string.format("{\\1a&HFF&\\3a&H%02X&\\3c&H%02X%02X%02X&\\bord1}", 0, line_color, line_color, line_color)
- local handle_hilight_format = string.format("{\\1a&H%02X&\\3a&H%02X&\\3c&H%02X%02X%02X&\\bord0}", 230, 0, line_color, line_color, line_color)
- local handle_drag_format = string.format("{\\1a&H%02X&\\3a&H%02X&\\3c&H%02X%02X%02X&\\bord1}", 200, 0, line_color, line_color, line_color)
-
- -- Main crop box
- ass:new_event()
- ass:pos(0,0)
- ass:append( box_format )
- ass:draw_start()
- ass:rect_cw(s_crop[1], s_crop[2], s_crop[3], s_crop[4])
- ass:draw_stop()
-
- -- Guide grid, 3x3
- if self.options.guide_type then
- ass:new_event()
- ass:pos(0,0)
- ass:append( guide_format )
- ass:draw_start()
-
- local w = (s_crop[3] - s_crop[1])
- local h = (s_crop[4] - s_crop[2])
-
- local w_3rd = w / 3
- local h_3rd = h / 3
- local w_2 = w / 2
- local h_2 = h / 2
- if self.options.guide_type == 1 then
- -- 3x3 grid
- ass:move_to(s_crop[1] + w_3rd, s_crop[2])
- ass:line_to(s_crop[1] + w_3rd, s_crop[4])
-
- ass:move_to(s_crop[1] + w_3rd*2, s_crop[2])
- ass:line_to(s_crop[1] + w_3rd*2, s_crop[4])
-
- ass:move_to(s_crop[1], s_crop[2] + h_3rd)
- ass:line_to(s_crop[3], s_crop[2] + h_3rd)
-
- ass:move_to(s_crop[1], s_crop[2] + h_3rd*2)
- ass:line_to(s_crop[3], s_crop[2] + h_3rd*2)
-
- elseif self.options.guide_type == 2 then
- -- Top to bottom
- ass:move_to(s_crop[1] + w_2, s_crop[2])
- ass:line_to(s_crop[1] + w_2, s_crop[4])
-
- -- Left to right
- ass:move_to(s_crop[1], s_crop[2] + h_2)
- ass:line_to(s_crop[3], s_crop[2] + h_2)
- end
- ass:draw_stop()
- end
-
- if self.dragging > 0 and drawn_handle ~= 5 then
- -- While dragging, draw only the dragging handle
- ass:new_event()
- ass:append( handle_drag_format )
- ass:pos(0,0)
- ass:draw_start()
- ass:rect_cw(s_hb[drawn_handle][1], s_hb[drawn_handle][2], s_hb[drawn_handle][3], s_hb[drawn_handle][4])
- ass:draw_stop()
- elseif self.dragging == 0 then
- local hit_index = self:hit_test(s_hb, self.mouse_screen)
- if hit_index > 0 and hit_index ~= 5 then
- -- Hilight handle
- ass:new_event()
- ass:append( handle_hilight_format )
- ass:pos(0,0)
- ass:draw_start()
- ass:rect_cw(s_hb[hit_index][1], s_hb[hit_index][2], s_hb[hit_index][3], s_hb[hit_index][4])
- ass:draw_stop()
- end
-
- ass:new_event()
- ass:pos(0,0)
- ass:append( box_format )
- ass:draw_start()
-
- -- Draw corner handles
- for k, v in pairs({1, 3, 7, 9}) do
- ass:rect_cw(s_hb[v][1], s_hb[v][2], s_hb[v][3], s_hb[v][4])
- end
- ass:draw_stop()
- end
-
- if true or draw_text then
-
- local br_pos = {s_crop[3] - 2, s_crop[4] + 2}
- local br_align = 9
- if br_pos[2] >= self.display_state.screen.height - 20 then
- br_pos[2] = br_pos[2] - 4
- br_align = 3
- end
-
- ass:new_event()
- ass:pos(unpack(br_pos))
- ass:an( br_align )
- ass:append("{\\fs20\\shad0\\be0\\bord2}")
- ass:append(string.format("%dx%d", v_crop[3] - v_crop[1], v_crop[4] - v_crop[2]) )
-
- local tl_pos = {s_crop[1] + 2, s_crop[2] - 2}
- local tl_align = 1
- if tl_pos[2] < 20 then
- tl_pos[2] = tl_pos[2] + 4
- tl_align = 7
- end
-
- ass:new_event()
- ass:pos(unpack(tl_pos))
- ass:an( tl_align )
- ass:append("{\\fs20\\shad0\\be0\\bord2}")
- ass:append(string.format("%d,%d", v_crop[1], v_crop[2]))
- end
-
- ass:draw_stop()
- end
-
- -- Crosshair for mouse
- if self.options.draw_mouse and not dim_only then
- ass:new_event()
- ass:pos(0,0)
- ass:append( guide_format )
- ass:draw_start()
-
- ass:move_to(self.mouse_screen.x, 0)
- ass:line_to(self.mouse_screen.x, self.display_state.screen.height)
-
- ass:move_to(0, self.mouse_screen.y)
- ass:line_to(self.display_state.screen.width, self.mouse_screen.y)
-
- ass:draw_stop()
- end
-
- if self.options.draw_help and not dim_only then
- ass:new_event()
- ass:pos(self.display_state.screen.width - 5, 5)
- local text_align = 9
- ass:append( string.format("{\\fs%d\\an%d\\bord2}", self.text_size, text_align) )
-
- local fmt_key = function( key, text ) return string.format("[{\\c&HBEBEBE&}%s{\\c} %s]", key:upper(), text) end
-
- local crosshair_txt = self.options.draw_mouse and "Hide" or "Show";
- lines = {
- fmt_key("ENTER", "Accept crop") .. " " .. fmt_key("ESC", "Cancel crop") .. " " .. fmt_key("D", "Autodetect crop") .. " " .. fmt_key("T", "Test crop"),
- fmt_key("SHIFT-Drag", "Constrain ratio") .. " " .. fmt_key("SHIFT-Arrow", "Nudge"),
- fmt_key("C", crosshair_txt .. " crosshair") .. " " .. fmt_key("X", "Cycle guides") .. " " .. fmt_key("Z", "Invert color"),
- }
-
- local full_line = nil
- for i, line in pairs(lines) do
- if line ~= nil then
- full_line = full_line and (full_line .. "\\N" .. line) or line
- end
- end
- ass:append(full_line)
- end
-
- return ass.text
+ if not self.display_state.video_ready then
+ msg.info("No video info on display_state")
+ return ""
+ end
+
+ line_color = self.options.color_invert and 20 or 220
+ local guide_format = string.format(
+ "{\\3a&HFF&\\3a&H%02X&\\3c&H%02X%02X%02X&\\bord1\\shad0}",
+ 128,
+ line_color,
+ line_color,
+ line_color
+ )
+
+ ass = assdraw.ass_new()
+ if self.current_crop then
+ if self.testing_crop then
+ -- Just draw simple help
+ ass:new_event()
+ ass:pos(self.display_state.screen.width - 5, 5)
+ ass:append(string.format("{\\fs%d\\an%d\\bord2}", self.text_size, 9))
+
+ local fmt_key = function(key, text)
+ return string.format("[{\\c&HBEBEBE&}%s{\\c} %s]", key:upper(), text)
+ end
+
+ ass:append(
+ fmt_key("ENTER", "Accept crop")
+ .. " "
+ .. fmt_key("ESC", "Cancel crop")
+ .. "\\N"
+ .. fmt_key("T", "Stop testing")
+ )
+ return ass.text
+ end
+
+ local temp_crop, drawn_handle = self:offset_crop_by_drag()
+ local v_hb = self:get_hitboxes(temp_crop)
+ -- Map coords to screen
+ local s_hb = {}
+ for index, coords in pairs(v_hb) do
+ local x1, y1 = self.display_state:video_to_screen(coords[1], coords[2])
+ local x2, y2 = self.display_state:video_to_screen(coords[3], coords[4])
+ s_hb[index] = { x1, y1, x2, y2 }
+ end
+
+ -- Full crop
+ local v_crop = v_hb[10] -- Video-space
+ local s_crop = s_hb[10] -- Screen-space
+
+ -- Inverse clipping for the crop box
+ ass:new_event()
+ ass:append(string.format("{\\iclip(%d,%d,%d,%d)}", s_crop[1], s_crop[2], s_crop[3], s_crop[4]))
+
+ -- Dim overlay
+ local format_dim = string.format(
+ "{\\bord0\\1a&H%02X&\\1c&H%02X%02X%02X&}",
+ self.overlay_transparency,
+ self.overlay_lightness,
+ self.overlay_lightness,
+ self.overlay_lightness
+ )
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:append(format_dim)
+ ass:rect_cw(0, 0, self.display_state.screen.width, self.display_state.screen.height)
+ ass:draw_stop()
+
+ if dim_only then -- Early out with just the dim outline
+ return ass.text
+ end
+
+ if draw_text then
+ -- Text on end
+ ass:new_event()
+ ass:pos(ce_x, ce_y)
+ -- Text align
+ local txt_a = ((ce_x > cs_x) and 3 or 1) + ((ce_y > cs_y) and 0 or 6)
+ ass:an(txt_a)
+ ass:append("{\\fs20\\shad0\\be0\\bord2}")
+ ass:append(string.format("%dx%d", math.abs(ce_x - cs_x), math.abs(ce_y - cs_y)))
+ end
+
+ local box_format =
+ string.format("{\\1a&HFF&\\3a&H%02X&\\3c&H%02X%02X%02X&\\bord1}", 0, line_color, line_color, line_color)
+ local handle_hilight_format = string.format(
+ "{\\1a&H%02X&\\3a&H%02X&\\3c&H%02X%02X%02X&\\bord0}",
+ 230,
+ 0,
+ line_color,
+ line_color,
+ line_color
+ )
+ local handle_drag_format = string.format(
+ "{\\1a&H%02X&\\3a&H%02X&\\3c&H%02X%02X%02X&\\bord1}",
+ 200,
+ 0,
+ line_color,
+ line_color,
+ line_color
+ )
+
+ -- Main crop box
+ ass:new_event()
+ ass:pos(0, 0)
+ ass:append(box_format)
+ ass:draw_start()
+ ass:rect_cw(s_crop[1], s_crop[2], s_crop[3], s_crop[4])
+ ass:draw_stop()
+
+ -- Guide grid, 3x3
+ if self.options.guide_type then
+ ass:new_event()
+ ass:pos(0, 0)
+ ass:append(guide_format)
+ ass:draw_start()
+
+ local w = (s_crop[3] - s_crop[1])
+ local h = (s_crop[4] - s_crop[2])
+
+ local w_3rd = w / 3
+ local h_3rd = h / 3
+ local w_2 = w / 2
+ local h_2 = h / 2
+ if self.options.guide_type == 1 then
+ -- 3x3 grid
+ ass:move_to(s_crop[1] + w_3rd, s_crop[2])
+ ass:line_to(s_crop[1] + w_3rd, s_crop[4])
+
+ ass:move_to(s_crop[1] + w_3rd * 2, s_crop[2])
+ ass:line_to(s_crop[1] + w_3rd * 2, s_crop[4])
+
+ ass:move_to(s_crop[1], s_crop[2] + h_3rd)
+ ass:line_to(s_crop[3], s_crop[2] + h_3rd)
+
+ ass:move_to(s_crop[1], s_crop[2] + h_3rd * 2)
+ ass:line_to(s_crop[3], s_crop[2] + h_3rd * 2)
+ elseif self.options.guide_type == 2 then
+ -- Top to bottom
+ ass:move_to(s_crop[1] + w_2, s_crop[2])
+ ass:line_to(s_crop[1] + w_2, s_crop[4])
+
+ -- Left to right
+ ass:move_to(s_crop[1], s_crop[2] + h_2)
+ ass:line_to(s_crop[3], s_crop[2] + h_2)
+ end
+ ass:draw_stop()
+ end
+
+ if self.dragging > 0 and drawn_handle ~= 5 then
+ -- While dragging, draw only the dragging handle
+ ass:new_event()
+ ass:append(handle_drag_format)
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(s_hb[drawn_handle][1], s_hb[drawn_handle][2], s_hb[drawn_handle][3], s_hb[drawn_handle][4])
+ ass:draw_stop()
+ elseif self.dragging == 0 then
+ local hit_index = self:hit_test(s_hb, self.mouse_screen)
+ if hit_index > 0 and hit_index ~= 5 then
+ -- Hilight handle
+ ass:new_event()
+ ass:append(handle_hilight_format)
+ ass:pos(0, 0)
+ ass:draw_start()
+ ass:rect_cw(s_hb[hit_index][1], s_hb[hit_index][2], s_hb[hit_index][3], s_hb[hit_index][4])
+ ass:draw_stop()
+ end
+
+ ass:new_event()
+ ass:pos(0, 0)
+ ass:append(box_format)
+ ass:draw_start()
+
+ -- Draw corner handles
+ for k, v in pairs({ 1, 3, 7, 9 }) do
+ ass:rect_cw(s_hb[v][1], s_hb[v][2], s_hb[v][3], s_hb[v][4])
+ end
+ ass:draw_stop()
+ end
+
+ if true or draw_text then
+ local br_pos = { s_crop[3] - 2, s_crop[4] + 2 }
+ local br_align = 9
+ if br_pos[2] >= self.display_state.screen.height - 20 then
+ br_pos[2] = br_pos[2] - 4
+ br_align = 3
+ end
+
+ ass:new_event()
+ ass:pos(unpack(br_pos))
+ ass:an(br_align)
+ ass:append("{\\fs20\\shad0\\be0\\bord2}")
+ ass:append(string.format("%dx%d", v_crop[3] - v_crop[1], v_crop[4] - v_crop[2]))
+
+ local tl_pos = { s_crop[1] + 2, s_crop[2] - 2 }
+ local tl_align = 1
+ if tl_pos[2] < 20 then
+ tl_pos[2] = tl_pos[2] + 4
+ tl_align = 7
+ end
+
+ ass:new_event()
+ ass:pos(unpack(tl_pos))
+ ass:an(tl_align)
+ ass:append("{\\fs20\\shad0\\be0\\bord2}")
+ ass:append(string.format("%d,%d", v_crop[1], v_crop[2]))
+ end
+
+ ass:draw_stop()
+ end
+
+ -- Crosshair for mouse
+ if self.options.draw_mouse and not dim_only then
+ ass:new_event()
+ ass:pos(0, 0)
+ ass:append(guide_format)
+ ass:draw_start()
+
+ ass:move_to(self.mouse_screen.x, 0)
+ ass:line_to(self.mouse_screen.x, self.display_state.screen.height)
+
+ ass:move_to(0, self.mouse_screen.y)
+ ass:line_to(self.display_state.screen.width, self.mouse_screen.y)
+
+ ass:draw_stop()
+ end
+
+ if self.options.draw_help and not dim_only then
+ ass:new_event()
+ ass:pos(self.display_state.screen.width - 5, 5)
+ local text_align = 9
+ ass:append(string.format("{\\fs%d\\an%d\\bord2}", self.text_size, text_align))
+
+ local fmt_key = function(key, text)
+ return string.format("[{\\c&HBEBEBE&}%s{\\c} %s]", key:upper(), text)
+ end
+
+ local crosshair_txt = self.options.draw_mouse and "Hide" or "Show"
+ lines = {
+ fmt_key("ENTER", "Accept crop") .. " " .. fmt_key("ESC", "Cancel crop") .. " " .. fmt_key(
+ "^D",
+ "Autodetect crop"
+ ) .. " " .. fmt_key("^T", "Test crop"),
+ fmt_key("SHIFT-Drag", "Constrain ratio") .. " " .. fmt_key("SHIFT-Arrow", "Nudge"),
+ fmt_key("^C", crosshair_txt .. " crosshair") .. " " .. fmt_key("^X", "Cycle guides") .. " " .. fmt_key(
+ "^Z",
+ "Invert color"
+ ),
+ }
+
+ local full_line = nil
+ for i, line in pairs(lines) do
+ if line ~= nil then
+ full_line = full_line and (full_line .. "\\N" .. line) or line
+ end
+ end
+ ass:append(full_line)
+ end
+
+ return ass.text
end
--[[
A tool to expand properties in template strings, mimicking mpv's
property expansion but with a few extras (like formatting times).
Depends on helpers.lua (isempty)
-]]--
+]]
+--
local PropertyExpander = {}
PropertyExpander.__index = PropertyExpander
setmetatable(PropertyExpander, {
- __call = function (cls, ...) return cls.new(...) end
+ __call = function(cls, ...)
+ return cls.new(...)
+ end,
})
function PropertyExpander.new(property_source)
- local self = setmetatable({}, PropertyExpander)
- self.sentinel = {}
+ local self = setmetatable({}, PropertyExpander)
+ self.sentinel = {}
- -- property_source is a table which defines the following functions:
- -- get_raw_property(name, def) - returns a raw property or def
- -- get_property(name) - returns a string
- -- get_property_osd(name) - returns an OSD formatted string (whatever that'll mean)
- self.property_source = property_source
- return self
+ -- property_source is a table which defines the following functions:
+ -- get_raw_property(name, def) - returns a raw property or def
+ -- get_property(name) - returns a string
+ -- get_property_osd(name) - returns an OSD formatted string (whatever that'll mean)
+ self.property_source = property_source
+ return self
end
-
-- Formats seconds to H:M:S based on a %h-%m-%s format
function PropertyExpander:_format_time(seconds, time_format)
- -- In case "seconds" is not a number, give it back
- if type(seconds) ~= "number" then
- return seconds
- end
+ -- In case "seconds" is not a number, give it back
+ if type(seconds) ~= "number" then
+ return seconds
+ end
- time_format = time_format or "%02h.%02m.%06.3s"
+ time_format = time_format or "%02h.%02m.%06.3s"
- local types = { h='d', m='d', s='f', S='f', M='d' }
- local values = {
- h=math.floor(seconds / 3600),
- m=math.floor((seconds % 3600) / 60),
- s=(seconds % 60),
- S=seconds,
- M=math.floor((seconds % 1)*1000)
- }
+ local types = { h = "d", m = "d", s = "f", S = "f", M = "d" }
+ local values = {
+ h = math.floor(seconds / 3600),
+ m = math.floor((seconds % 3600) / 60),
+ s = (seconds % 60),
+ S = seconds,
+ M = math.floor((seconds % 1) * 1000),
+ }
- local substitutor = function(sub_format, char)
- local v = values[char]
- local t = types[char]
- if t == nil then return nil end
+ local substitutor = function(sub_format, char)
+ local v = values[char]
+ local t = types[char]
+ if t == nil then
+ return nil
+ end
- sub_format = '%' .. sub_format .. types[char]
- return v and sub_format:format(v) or nil
- end
+ sub_format = "%" .. sub_format .. types[char]
+ return v and sub_format:format(v) or nil
+ end
- return time_format:gsub('%%([%-%+ #0]*%d*.?%d*)([%a%%])', substitutor)
+ return time_format:gsub("%%([%-%+ #0]*%d*.?%d*)([%a%%])", substitutor)
end
-- Format a date
function PropertyExpander:_format_date(seconds, date_format)
- -- In case "seconds" is not nil or a number, give it back
- if type(seconds) ~= "number" and type(seconds) ~= "nil" then
- return seconds
- end
+ -- In case "seconds" is not nil or a number, give it back
+ if type(seconds) ~= "number" and type(seconds) ~= "nil" then
+ return seconds
+ end
- --[[
+ --[[
As stated by Lua docs:
%a abbreviated weekday name (e.g., Wed)
%A full weekday name (e.g., Wednesday)
@@ -2736,393 +2963,430 @@ function PropertyExpander:_format_date(seconds, date_format)
%Y full year (1998)
%y two-digit year (98) [00-99]
%% the character `%ยด
- ]]--
- date_format = date_format or "%Y-%m-%d %H-%M-%S"
- return os.date(date_format, seconds)
+ ]]
+ --
+ date_format = date_format or "%Y-%m-%d %H-%M-%S"
+ return os.date(date_format, seconds)
end
-
function PropertyExpander:expand(format_string)
- local comparisons = {
- {
- -- Less than or equal
- '^(..-)<=(.+)$',
- function(property_value, other_value)
- if type(property_value) ~= "number" then return nil end
- return property_value <= tonumber(other_value)
- end
- },
- {
- -- More than or equal
- '^(..-)>=(.+)$',
- function(property_value, other_value)
- if type(property_value) ~= "number" then return nil end
- return property_value >= tonumber(other_value)
- end
- },
- {
- -- Less than
- '^(..-)<(.+)$',
- function(property_value, other_value)
- if type(property_value) ~= "number" then return nil end
- return property_value < tonumber(other_value)
- end
- },
- {
- -- More than
- '^(..-)>(.+)$',
- function(property_value, other_value)
- if type(property_value) ~= "number" then return nil end
- return property_value > tonumber(other_value)
- end
- },
- {
- -- Equal
- '^(..-)==(.+)$',
- function(property_value, other_value)
- if type(property_value) == "number" then
- other_value = tonumber(other_value)
- elseif type(property_value) ~= "string" then
- -- Ignore booleans and others
- return nil
- end
- return property_value == other_value
- end
- },
- {
- -- Starts with
- '^(..-)^=(.+)$',
- function(property_value, other_value)
- if type(property_value) ~= "string" then return nil end
- return property_value:sub(1, other_value:len()) == other_value
- end
- },
- {
- -- Ends with
- '^(..-)$=(.+)$',
- function(property_value, other_value)
- if type(property_value) ~= "string" then return nil end
- return other_value == '' or property_value:sub(-other_value:len()) == other_value
- end
- },
- {
- -- Contains
- '^(..-)~=(.+)$',
- function(property_value, other_value)
- if type(property_value) ~= "string" then return nil end
- return property_value:find(other_value, nil, true) ~= nil
- end
- },
- }
-
- local substitutor = function(match)
- local command, inner = match:sub(3, -2):match('^([%?!~^%%#&]?)(.+)$')
- local colon_index = inner:find(':')
-
- local property_name = inner
- local secondary = ""
- local has_colon = colon_index and true or false
-
- if colon_index then
- property_name = inner:sub(1, colon_index-1)
- secondary = inner:sub(colon_index+1, -1)
- end
-
- local used_comparison = nil
- local comparison_value = nil
- for i, comparison in ipairs(comparisons) do
- local name, other_value = property_name:match(comparison[1])
- if name then
- property_name = name
- comparison_value = other_value
- used_comparison = comparison[2]
- break
- end
- end
-
- local raw_property_value = self.property_source:get_raw_property(property_name, self.sentinel)
- local property_exists = raw_property_value ~= self.sentinel
-
- if command == '' then
- if used_comparison then
- if used_comparison(raw_property_value, comparison_value) then return self:expand(secondary)
- else return '' end
- end
-
- -- Return the property value if it's not nil, else the (expanded) secondary
- return property_exists and self.property_source:get_property(property_name) or self:expand(secondary)
-
-
- elseif command == '?' then
- -- Return (expanded) secondary if property is truthy (sentinel is falsey)
- if not isempty(raw_property_value) then return self:expand(secondary) else return '' end
-
- elseif command == '!' then
- if used_comparison then
- if not used_comparison(raw_property_value, comparison_value) then return self:expand(secondary)
- else return '' end
- end
-
- -- Return (expanded) secondary if property is falsey
- if isempty(raw_property_value) then return self:expand(secondary) else return '' end
-
-
- elseif command == '^' then
- -- Return (expanded) secondary if property does not exist
- return not property_exists and self:expand(secondary) or ""
-
-
- elseif command == '%' then
- -- Return the value formatted using the secondary string
- return secondary:format(raw_property_value)
-
- elseif command == '#' then
- -- Format a number to HMS
- return self:_format_time(raw_property_value, has_colon and secondary or nil)
-
- elseif command == '&' then
- -- Format a date
- return self:_format_date(nil, has_colon and secondary or nil)
-
-
- elseif command == '@' then
- -- Format the value for OSD - mostly useful for latching onto mpv's properties
- return property_exists and self.property_source:get_property_osd(property_name) or self:expand(secondary)
- end
-
- end
-
- -- Lua patterns are generally a pain, but %b is comfy!
- local expanded = format_string:gsub('%$%b{}', substitutor)
- return expanded
+ local comparisons = {
+ {
+ -- Less than or equal
+ "^(..-)<=(.+)$",
+ function(property_value, other_value)
+ if type(property_value) ~= "number" then
+ return nil
+ end
+ return property_value <= tonumber(other_value)
+ end,
+ },
+ {
+ -- More than or equal
+ "^(..-)>=(.+)$",
+ function(property_value, other_value)
+ if type(property_value) ~= "number" then
+ return nil
+ end
+ return property_value >= tonumber(other_value)
+ end,
+ },
+ {
+ -- Less than
+ "^(..-)<(.+)$",
+ function(property_value, other_value)
+ if type(property_value) ~= "number" then
+ return nil
+ end
+ return property_value < tonumber(other_value)
+ end,
+ },
+ {
+ -- More than
+ "^(..-)>(.+)$",
+ function(property_value, other_value)
+ if type(property_value) ~= "number" then
+ return nil
+ end
+ return property_value > tonumber(other_value)
+ end,
+ },
+ {
+ -- Equal
+ "^(..-)==(.+)$",
+ function(property_value, other_value)
+ if type(property_value) == "number" then
+ other_value = tonumber(other_value)
+ elseif type(property_value) ~= "string" then
+ -- Ignore booleans and others
+ return nil
+ end
+ return property_value == other_value
+ end,
+ },
+ {
+ -- Starts with
+ "^(..-)^=(.+)$",
+ function(property_value, other_value)
+ if type(property_value) ~= "string" then
+ return nil
+ end
+ return property_value:sub(1, other_value:len()) == other_value
+ end,
+ },
+ {
+ -- Ends with
+ "^(..-)$=(.+)$",
+ function(property_value, other_value)
+ if type(property_value) ~= "string" then
+ return nil
+ end
+ return other_value == "" or property_value:sub(-other_value:len()) == other_value
+ end,
+ },
+ {
+ -- Contains
+ "^(..-)~=(.+)$",
+ function(property_value, other_value)
+ if type(property_value) ~= "string" then
+ return nil
+ end
+ return property_value:find(other_value, nil, true) ~= nil
+ end,
+ },
+ }
+
+ local substitutor = function(match)
+ local command, inner = match:sub(3, -2):match("^([%?!~^%%#&]?)(.+)$")
+ local colon_index = inner:find(":")
+
+ local property_name = inner
+ local secondary = ""
+ local has_colon = colon_index and true or false
+
+ if colon_index then
+ property_name = inner:sub(1, colon_index - 1)
+ secondary = inner:sub(colon_index + 1, -1)
+ end
+
+ local used_comparison = nil
+ local comparison_value = nil
+ for i, comparison in ipairs(comparisons) do
+ local name, other_value = property_name:match(comparison[1])
+ if name then
+ property_name = name
+ comparison_value = other_value
+ used_comparison = comparison[2]
+ break
+ end
+ end
+
+ local raw_property_value = self.property_source:get_raw_property(property_name, self.sentinel)
+ local property_exists = raw_property_value ~= self.sentinel
+
+ if command == "" then
+ if used_comparison then
+ if used_comparison(raw_property_value, comparison_value) then
+ return self:expand(secondary)
+ else
+ return ""
+ end
+ end
+
+ -- Return the property value if it's not nil, else the (expanded) secondary
+ return property_exists and self.property_source:get_property(property_name) or self:expand(secondary)
+ elseif command == "?" then
+ -- Return (expanded) secondary if property is truthy (sentinel is falsey)
+ if not isempty(raw_property_value) then
+ return self:expand(secondary)
+ else
+ return ""
+ end
+ elseif command == "!" then
+ if used_comparison then
+ if not used_comparison(raw_property_value, comparison_value) then
+ return self:expand(secondary)
+ else
+ return ""
+ end
+ end
+
+ -- Return (expanded) secondary if property is falsey
+ if isempty(raw_property_value) then
+ return self:expand(secondary)
+ else
+ return ""
+ end
+ elseif command == "^" then
+ -- Return (expanded) secondary if property does not exist
+ return not property_exists and self:expand(secondary) or ""
+ elseif command == "%" then
+ -- Return the value formatted using the secondary string
+ return secondary:format(raw_property_value)
+ elseif command == "#" then
+ -- Format a number to HMS
+ return self:_format_time(raw_property_value, has_colon and secondary or nil)
+ elseif command == "&" then
+ -- Format a date
+ return self:_format_date(nil, has_colon and secondary or nil)
+ elseif command == "@" then
+ -- Format the value for OSD - mostly useful for latching onto mpv's properties
+ return property_exists and self.property_source:get_property_osd(property_name) or self:expand(secondary)
+ end
+ end
+
+ -- Lua patterns are generally a pain, but %b is comfy!
+ local expanded = format_string:gsub("%$%b{}", substitutor)
+ return expanded
end
-
local MPVPropertySource = {}
MPVPropertySource.__index = MPVPropertySource
setmetatable(MPVPropertySource, {
- __call = function (cls, ...) return cls.new(...) end
+ __call = function(cls, ...)
+ return cls.new(...)
+ end,
})
function MPVPropertySource.new(values)
- local self = setmetatable({}, MPVPropertySource)
- self.values = values
+ local self = setmetatable({}, MPVPropertySource)
+ self.values = values
- return self
+ return self
end
function MPVPropertySource:get_raw_property(name, default)
- if name:find('mpv/') ~= nil then
- return mp.get_property_native(name:sub(5), default)
- end
- local v = self.values[name]
- if v ~= nil then return v else return default end
+ if name:find("mpv/") ~= nil then
+ return mp.get_property_native(name:sub(5), default)
+ end
+ local v = self.values[name]
+ if v ~= nil then
+ return v
+ else
+ return default
+ end
end
function MPVPropertySource:get_property(name, default)
- if name:find('mpv/') ~= nil then
- return mp.get_property(name:sub(5), default)
- end
- local v = self.values[name]
- if v ~= nil then return tostring(v) else return default end
+ if name:find("mpv/") ~= nil then
+ return mp.get_property(name:sub(5), default)
+ end
+ local v = self.values[name]
+ if v ~= nil then
+ return tostring(v)
+ else
+ return default
+ end
end
function MPVPropertySource:get_property_osd(name, default)
- if name:find('mpv/') ~= nil then
- return mp.get_property_osd(name:sub(5), default)
- end
- local v = self.values[name]
- if v ~= nil then return tostring(v) else return default end
+ if name:find("mpv/") ~= nil then
+ return mp.get_property_osd(name:sub(5), default)
+ end
+ local v = self.values[name]
+ if v ~= nil then
+ return tostring(v)
+ else
+ return default
+ end
end
function script_crop_toggle()
- if asscropper.active then
- asscropper:stop_crop(true)
- else
- local on_crop = function(crop)
- mp.set_osd_ass(0, 0, "")
- screenshot(crop)
- end
- local on_cancel = function()
- mp.osd_message("Crop canceled")
- mp.set_osd_ass(0, 0, "")
- end
-
- local crop_options = {
- guide_type = ({none=0, grid=1, center=2})[option_values.guide_type],
- draw_mouse = option_values.draw_mouse,
- color_invert = option_values.color_invert,
- auto_invert = option_values.auto_invert
- }
- asscropper:start_crop(crop_options, on_crop, on_cancel)
- if not asscropper.active then
- mp.osd_message("No video to crop!", 2)
- end
- end
+ if asscropper.active then
+ asscropper:stop_crop(true)
+ else
+ local on_crop = function(crop)
+ mp.set_osd_ass(0, 0, "")
+ screenshot(crop)
+ end
+ local on_cancel = function()
+ mp.osd_message("Crop canceled")
+ mp.set_osd_ass(0, 0, "")
+ end
+
+ local crop_options = {
+ guide_type = ({ none = 0, grid = 1, center = 2 })[option_values.guide_type],
+ draw_mouse = option_values.draw_mouse,
+ color_invert = option_values.color_invert,
+ auto_invert = option_values.auto_invert,
+ }
+ asscropper:start_crop(crop_options, on_crop, on_cancel)
+ if not asscropper.active then
+ mp.osd_message("No video to crop!", 2)
+ end
+ end
end
-
local next_tick_time = nil
function on_tick_listener()
- local now = mp.get_time()
- if next_tick_time == nil or now >= next_tick_time then
- if asscropper.active and display_state:recalculate_bounds() then
- mp.set_osd_ass(display_state.screen.width, display_state.screen.height, asscropper:get_render_ass())
- end
- next_tick_time = now + (1/60)
- end
+ local now = mp.get_time()
+ if next_tick_time == nil or now >= next_tick_time then
+ if asscropper.active and display_state:recalculate_bounds() then
+ mp.set_osd_ass(display_state.screen.width, display_state.screen.height, asscropper:get_render_ass())
+ end
+ next_tick_time = now + (1 / 60)
+ end
end
+local function resolve_path(path)
+ -- Expand tilde (~) to home directory
+ local home = os.getenv("HOME")
+ if home and path:sub(1, 2) == "~/" then
+ path = home .. path:sub(2)
+ end
+ -- Convert relative paths to absolute paths
+ return mp.command_native({ "expand-path", path })
+end
function expand_output_path(cropbox)
- local filename = mp.get_property_native("filename")
- local playback_time = mp.get_property_native("playback-time")
- local duration = mp.get_property_native("duration")
-
- local filename_without_ext, extension = filename:match("^(.+)%.(.-)$")
-
- local properties = {
- path = mp.get_property_native("path"), -- Original path
-
- filename = filename_without_ext or filename, -- Filename without extension (or filename if no dots
- file_ext = extension or "", -- Original extension without leading dot (or empty string)
-
- pos = mp.get_property_native("playback-time"),
-
- full = false,
- is_image = (duration == 0 and playback_time == 0),
-
- crop_w = cropbox.w,
- crop_h = cropbox.h,
- crop_x = cropbox.x,
- crop_y = cropbox.y,
- crop_x2 = cropbox.x2,
- crop_y2 = cropbox.y2,
-
- unique = 0,
-
- ext = option_values.output_extension
- }
- local propex = PropertyExpander(MPVPropertySource(properties))
-
-
- local test_path = propex:expand(option_values.output_template)
- -- If the paths do not change when incrementing the unique, it's not used.
- -- Return early and avoid the endless loop
- properties.unique = 1
- if propex:expand(option_values.output_template) == test_path then
- properties.full = true
- local temporary_screenshot_path = propex:expand(option_values.output_template)
- return test_path, temporary_screenshot_path
-
- else
- -- Figure out an unique filename
- while true do
- test_path = propex:expand(option_values.output_template)
-
- -- Check if filename is free
- if not path_exists(test_path) then
- properties.full = true
- local temporary_screenshot_path = propex:expand(option_values.output_template)
- return test_path, temporary_screenshot_path
- else
- -- Try the next one
- properties.unique = properties.unique + 1
- end
- end
- end
+ local filename = mp.get_property_native("filename")
+ local playback_time = mp.get_property_native("playback-time")
+ local duration = mp.get_property_native("duration")
+
+ local filename_without_ext, extension = filename:match("^(.+)%.(.-)$")
+
+ local properties = {
+ path = mp.get_property_native("path"), -- Original path
+
+ filename = filename_without_ext or filename, -- Filename without extension (or filename if no dots)
+ file_ext = extension or "", -- Original extension without leading dot (or empty string)
+
+ pos = playback_time,
+
+ full = false,
+ is_image = (duration == 0 and playback_time == 0),
+
+ crop_w = cropbox.w,
+ crop_h = cropbox.h,
+ crop_x = cropbox.x,
+ crop_y = cropbox.y,
+ crop_x2 = cropbox.x2,
+ crop_y2 = cropbox.y2,
+
+ unique = 0,
+
+ ext = option_values.output_extension,
+ }
+
+ local propex = PropertyExpander(MPVPropertySource(properties))
+
+ local test_path = resolve_path(propex:expand(option_values.output_template))
+ -- If the paths do not change when incrementing the unique, it's not used.
+ -- Return early and avoid the endless loop
+ properties.unique = 1
+ if resolve_path(propex:expand(option_values.output_template)) == test_path then
+ properties.full = true
+ local temporary_screenshot_path = resolve_path(propex:expand(option_values.output_template))
+ return test_path, temporary_screenshot_path
+ else
+ -- Figure out a unique filename
+ while true do
+ test_path = resolve_path(propex:expand(option_values.output_template))
+
+ -- Check if filename is free
+ if not path_exists(test_path) then
+ properties.full = true
+ local temporary_screenshot_path = resolve_path(propex:expand(option_values.output_template))
+ return test_path, temporary_screenshot_path
+ else
+ -- Try the next one
+ properties.unique = properties.unique + 1
+ end
+ end
+ end
end
-
function screenshot(crop)
- local size = round_dec(crop.w) .. "x" .. round_dec(crop.h)
-
- -- Bail on bad crop sizes
- if not (crop.w > 0 and crop.h > 0) then
- mp.osd_message("Bad crop (" .. size .. ")!")
- return
- end
-
- local output_path, temporary_screenshot_path = expand_output_path(crop)
-
- -- Optionally create directories
- if option_values.create_directories then
- local paths = {}
- paths[1] = path_utils.dirname(output_path)
- paths[2] = path_utils.dirname(temporary_screenshot_path)
-
- -- Check if we can read the paths
- for i, path in ipairs(paths) do
- local l, err = utils.readdir(path)
- if err then
- create_directories(path)
- end
- end
- end
-
- local playback_time = mp.get_property_native("playback-time")
- local duration = mp.get_property_native("duration")
-
- local input_path = nil
-
- if option_values.skip_screenshot_for_images and duration == 0 and playback_time == 0 then
- -- Seems to be an image (or at least static file)
- input_path = mp.get_property_native("path")
- temporary_screenshot_path = nil
- else
- -- Not an image, take a temporary screenshot
-
- -- In case the full-size output path is identical to the crop path,
- -- crudely make it different
- if temporary_screenshot_path == output_path then
- temporary_screenshot_path = temporary_screenshot_path .. "_full.png"
- end
-
- -- Temporarily lower the PNG compression
- local previous_png_compression = mp.get_property_native("screenshot-png-compression")
- mp.set_property_native("screenshot-png-compression", 0)
- -- Take the screenshot
- mp.commandv("raw", "no-osd", "screenshot-to-file", temporary_screenshot_path)
- -- Return the previous value
- mp.set_property_native("screenshot-png-compression", previous_png_compression)
-
- if not path_exists(temporary_screenshot_path) then
- msg.error("Failed to take screenshot: " .. temporary_screenshot_path)
- mp.osd_message("Unable to save screenshot")
- return
- end
-
- input_path = temporary_screenshot_path
- end
-
- local crop_string = string.format("%d:%d:%d:%d", crop.w, crop.h, crop.x, crop.y)
- local cmd = {
- args = {
- "mpv", input_path,
- "--no-config",
- "--vf=crop=" .. crop_string,
- "--frames=1",
- "--ovc=" .. option_values.output_format,
- "-o", output_path
- }
- }
-
- msg.info("Cropping: ", crop_string, output_path)
- local ret = utils.subprocess(cmd)
-
- if not option_values.keep_original and temporary_screenshot_path then
- os.remove(temporary_screenshot_path)
- end
-
- if ret.error or ret.status ~= 0 then
- mp.osd_message("Screenshot failed, see console for details")
- msg.error("Crop failed! mpv exit code: " .. tostring(ret.status))
- msg.error("mpv stdout:")
- msg.error(ret.stdout)
- else
- msg.info("Crop finished!")
- mp.osd_message("Took screenshot (" .. size .. ")")
- end
+ local size = round_dec(crop.w) .. "x" .. round_dec(crop.h)
+
+ -- Bail on bad crop sizes
+ if not (crop.w > 0 and crop.h > 0) then
+ mp.osd_message("Bad crop (" .. size .. ")!")
+ return
+ end
+
+ local output_path, temporary_screenshot_path = expand_output_path(crop)
+
+ -- Optionally create directories
+ if option_values.create_directories then
+ local paths = {}
+ paths[1] = path_utils.dirname(output_path)
+ paths[2] = path_utils.dirname(temporary_screenshot_path)
+
+ -- Check if we can read the paths
+ for i, path in ipairs(paths) do
+ local l, err = utils.readdir(path)
+ if err then
+ create_directories(path)
+ end
+ end
+ end
+
+ local playback_time = mp.get_property_native("playback-time")
+ local duration = mp.get_property_native("duration")
+
+ local input_path = nil
+
+ if option_values.skip_screenshot_for_images and duration == 0 and playback_time == 0 then
+ -- Seems to be an image (or at least static file)
+ input_path = mp.get_property_native("path")
+ temporary_screenshot_path = nil
+ else
+ -- Not an image, take a temporary screenshot
+
+ -- In case the full-size output path is identical to the crop path,
+ -- crudely make it different
+ if temporary_screenshot_path == output_path then
+ temporary_screenshot_path = temporary_screenshot_path .. "_full.png"
+ end
+
+ -- Temporarily lower the PNG compression
+ local previous_png_compression = mp.get_property_native("screenshot-png-compression")
+ mp.set_property_native("screenshot-png-compression", 0)
+ -- Take the screenshot
+ mp.commandv("raw", "no-osd", "screenshot-to-file", temporary_screenshot_path)
+ -- Return the previous value
+ mp.set_property_native("screenshot-png-compression", previous_png_compression)
+
+ if not path_exists(temporary_screenshot_path) then
+ msg.error("Failed to take screenshot: " .. temporary_screenshot_path)
+ mp.osd_message("Unable to save screenshot")
+ return
+ end
+
+ input_path = temporary_screenshot_path
+ end
+
+ local crop_string = string.format("%d:%d:%d:%d", crop.w, crop.h, crop.x, crop.y)
+ local cmd = {
+ args = {
+ "mpv",
+ input_path,
+ "--no-config",
+ "--vf=crop=" .. crop_string,
+ "--frames=1",
+ "--ovc=" .. option_values.output_format,
+ "-o",
+ output_path,
+ },
+ }
+
+ msg.info("Cropping: ", crop_string, output_path)
+ local ret = utils.subprocess(cmd)
+
+ if not option_values.keep_original and temporary_screenshot_path then
+ os.remove(temporary_screenshot_path)
+ end
+
+ if ret.error or ret.status ~= 0 then
+ mp.osd_message("Screenshot failed, see console for details")
+ msg.error("Crop failed! mpv exit code: " .. tostring(ret.status))
+ msg.error("mpv stdout:")
+ msg.error(ret.stdout)
+ else
+ msg.info("Crop finished!")
+ mp.osd_message("Took screenshot (" .. size .. ")")
+ end
end
----------------------
@@ -3130,33 +3394,34 @@ end
----------------------
-- Sanity-check output_template
-if option_values.warn_about_template and not option_values.output_template:find('%${ext}') then
- msg.warn("Output template missing ${ext}! If this is desired, set warn_about_template=yes in config!")
+if option_values.warn_about_template and not option_values.output_template:find("%${ext}") then
+ msg.warn("Output template missing ${ext}! If this is desired, set warn_about_template=yes in config!")
end
-- Short list of extensions for encoders
local ENCODER_EXTENSION_MAP = {
- png = "png",
- mjpeg = "jpg",
- targa = "tga",
- tiff = "tiff",
- gif = "gif", -- please don't
- bmp = "bmp",
- jpegls = "jpg",
- ljpeg = "jpg",
- jpeg2000 = "jp2",
+ png = "png",
+ mjpeg = "jpg",
+ targa = "tga",
+ tiff = "tiff",
+ gif = "gif", -- please don't
+ bmp = "bmp",
+ jpegls = "jpg",
+ ljpeg = "jpg",
+ jpeg2000 = "jp2",
}
-- Pick an extension if one was not provided
if option_values.output_extension == "" then
- local extension = ENCODER_EXTENSION_MAP[option_values.output_format]
- if not extension then
- msg.error("Unrecognized output format '" .. option_values.output_format .. "', unable to pick an extension! Bailing!")
- mp.osd_message("mpv_crop_script was unable to choose an extension, check your config", 3)
- end
- option_values.output_extension = extension
+ local extension = ENCODER_EXTENSION_MAP[option_values.output_format]
+ if not extension then
+ msg.error(
+ "Unrecognized output format '" .. option_values.output_format .. "', unable to pick an extension! Bailing!"
+ )
+ mp.osd_message("mpv_crop_script was unable to choose an extension, check your config", 3)
+ end
+ option_values.output_extension = extension
end
-
display_state = DisplayState()
asscropper = ASSCropper(display_state)
asscropper.overlay_transparency = option_values.overlay_transparency
@@ -3168,6 +3433,6 @@ mp.register_event("tick", on_tick_listener)
local used_keybind = SCRIPT_KEYBIND
-- Disable the default keybind if asked to
if option_values.disable_keybind then
- used_keybind = nil
+ used_keybind = nil
end
mp.add_key_binding(used_keybind, SCRIPT_HANDLER, script_crop_toggle)