Skip to content

Instantly share code, notes, and snippets.

@hooke007
Created September 13, 2025 13:24
Show Gist options
  • Select an option

  • Save hooke007/98f217195ac5f66150e08fe7d0d15a23 to your computer and use it in GitHub Desktop.

Select an option

Save hooke007/98f217195ac5f66150e08fe7d0d15a23 to your computer and use it in GitHub Desktop.
mpv-miv [deprecated]
local assdraw = require "mp.assdraw"
local usr_bars = "brightness,contrast,gamma,saturation,hue,"
local enabled = false
local active_bars = {}
local bar_being_dragged = nil
local stale = false
function split_comma(input)
local ret = {}
for str in string.gmatch(input, "([^,]+)") do
ret[#ret + 1] = str
end
return ret
end
function get_position_normalized(x, y, bar)
return (x - bar.x) / bar.w, (y - bar.y) / bar.h
end
function handle_mouse_move()
if not bar_being_dragged then return end
local bar = bar_being_dragged
local mx, my = mp.get_mouse_pos()
local nx, _ = get_position_normalized(mx, my, bar)
nx = math.max(0, math.min(nx, 1))
local val = math.floor(nx * (bar.max_value - bar.min_value) + bar.min_value + 0.5)
mp.set_property_number(bar.property, val)
mp.command("show-text \'" .. bar.property .. " ${" .. bar.property .. "}\'")
-- the observe_property call will take care of setting the value
end
function handle_mouse_right(table)
if table["event"] == "down" then
local mx, my = mp.get_mouse_pos()
for _, bar in ipairs(active_bars) do
local nx, ny = get_position_normalized(mx, my, bar)
if nx >= 0 and ny >= 0 and nx <= 1 and ny <= 1 then
bar_being_dragged = bar
local val = math.floor(nx * (bar.max_value - bar.min_value) + bar.min_value + 0.5)
mp.set_property_number(bar.property, val)
mp.command("show-text \'" .. bar.property .. " ${" .. bar.property .. "}\'")
mp.add_forced_key_binding("mouse_move", "mouse_move", handle_mouse_move)
break
end
end
elseif table["event"] == "up" then
mp.remove_key_binding("mouse_move")
bar_being_dragged = nil
end
end
function property_changed(prop, val)
for _, bar in ipairs(active_bars) do
if bar.property == prop then
bar.value = val
stale = true
break
end
end
end
function idle_handler()
if not stale then return end
stale = false
local a = assdraw.ass_new()
a:new_event()
a:append(string.format("{\\an0\\bord2\\shad0\\1a&00&\\1c&%s&}", "888888"))
a:pos(0, 0)
a:draw_start()
for _, bar in ipairs(active_bars) do
a:rect_cw(bar.x, bar.y, bar.x + bar.w, bar.y + bar.h)
end
a:new_event()
a:append(string.format("{\\an0\\bord2\\shad0\\1a&00&\\1c&%s&}", "dddddd"))
a:pos(0, 0)
a:draw_start()
for _, bar in ipairs(active_bars) do
if bar.value > bar.min_value then
local val_norm = (bar.value - bar.min_value) / (bar.max_value - bar.min_value)
a:rect_cw(bar.x, bar.y, bar.x + val_norm * bar.w, bar.y + bar.h)
end
end
for _, bar in ipairs(active_bars) do
a:new_event()
a:append("{\\an6\\fs40\\bord2}")
a:pos(bar.x - 8, bar.y + bar.h/2 - 2)
a:append(bar.property:sub(1,1):upper() .. bar.property:sub(2,-1))
end
local ww, wh = mp.get_osd_size()
mp.set_osd_ass(ww, wh, a.text)
end
function fix_position()
local ww, wh = mp.get_osd_size()
for i, bar in ipairs(active_bars) do
bar.x = ww / 5
bar.y = wh / 2 + i * 38
bar.w = ww - 2 * (ww / 5)
bar.h = 20
end
end
function dimensions_changed()
stale = true
fix_position()
end
function enable()
if enabled then return end
enabled = true
mp.add_forced_key_binding("MBTN_RIGHT", "mouse_right", handle_mouse_right, {complex=true})
for i, prop in ipairs(split_comma(usr_bars)) do
local prop_info = mp.get_property_native("option-info/" .. prop)
if not prop_info then
mp.msg.warn("Property \'" .. prop .. "\' does not exist")
elseif not prop_info.type == "Integer" then
mp.msg.warn("Property \'" .. prop .. "\' is not an integer")
else
mp.observe_property(prop, "native", property_changed)
active_bars[#active_bars + 1] = {
property = prop,
value = mp.get_property_number(prop),
min_value = prop_info.min,
max_value = prop_info.max,
}
end
end
stale = true
fix_position()
mp.observe_property("osd-dimensions", "native", dimensions_changed)
mp.register_idle(idle_handler)
end
function disable()
if not enabled then return end
enabled = false
active_bars = {}
bar_being_dragged = nil
mp.remove_key_binding("mouse_right")
mp.remove_key_binding("mouse_move")
mp.unobserve_property(property_changed)
mp.unobserve_property(dimensions_changed)
mp.unregister_idle(idle_handler)
mp.set_osd_ass(1280, 720, "")
end
function toggle()
if enabled then
disable()
else
enable()
end
end
function reset()
for _, prop in ipairs(split_comma(usr_bars)) do
local prop_info = mp.get_property_native("option-info/" .. prop)
if prop_info and prop_info["default-value"] then
mp.set_property(prop_info["name"], prop_info["default-value"])
end
end
end
mp.add_key_binding(nil, "enable", enable)
mp.add_key_binding(nil, "disable", disable)
mp.add_key_binding(nil, "toggle", toggle)
mp.add_key_binding(nil, "reset", reset)
local opts = {
drag_to_pan_margin = 50,
drag_to_pan_move_if_full_view = false,
pan_follows_cursor_margin = 50,
cursor_centric_zoom_margin = 50,
cursor_centric_zoom_auto_center = true,
cursor_centric_zoom_dezoom_if_full_view = false,
}
local options = require 'mp.options'
local msg = require 'mp.msg'
local assdraw = require 'mp.assdraw'
options.read_options(opts, nil, function() end)
function clamp(value, low, high)
if value <= low then
return low
elseif value >= high then
return high
else
return value
end
end
local cleanup = nil -- function set up by drag-to-pan/pan-follows cursor and must be called to clean lingering state
function drag_to_pan_handler(table)
if cleanup then
cleanup()
cleanup = nil
end
if table["event"] == "down" then
local dim = mp.get_property_native("osd-dimensions")
if not dim then return end
local mouse_pos_origin, video_pan_origin = {}, {}
local moved = false
mouse_pos_origin[1], mouse_pos_origin[2] = mp.get_mouse_pos()
video_pan_origin[1] = mp.get_property_number("video-pan-x")
video_pan_origin[2] = mp.get_property_number("video-pan-y")
local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
local margin = opts.drag_to_pan_margin
local move_up = true
local move_lateral = true
if not opts.drag_to_pan_move_if_full_view then
if dim.ml >= 0 and dim.mr >= 0 then
move_lateral = false
end
if dim.mt >= 0 and dim.mb >= 0 then
move_up = false
end
end
if not move_up and not move_lateral then return end
local idle = function()
if moved then
local mX, mY = mp.get_mouse_pos()
local pX = video_pan_origin[1]
local pY = video_pan_origin[2]
if move_lateral then
pX = video_pan_origin[1] + (mX - mouse_pos_origin[1]) / video_size[1]
if 2 * margin > dim.ml + dim.mr then
pX = clamp(pX,
(-margin + dim.w / 2) / video_size[1] - 0.5,
(margin - dim.w / 2) / video_size[1] + 0.5)
else
pX = clamp(pX,
(margin - dim.w / 2) / video_size[1] + 0.5,
(-margin + dim.w / 2) / video_size[1] - 0.5)
end
end
if move_up then
pY = video_pan_origin[2] + (mY - mouse_pos_origin[2]) / video_size[2]
if 2 * margin > dim.mt + dim.mb then
pY = clamp(pY,
(-margin + dim.h / 2) / video_size[2] - 0.5,
(margin - dim.h / 2) / video_size[2] + 0.5)
else
pY = clamp(pY,
(margin - dim.h / 2) / video_size[2] + 0.5,
(-margin + dim.h / 2) / video_size[2] - 0.5)
end
end
mp.command("no-osd set video-pan-x " .. clamp(pX, -3, 3) .. "; no-osd set video-pan-y " .. clamp(pY, -3, 3))
moved = false
end
end
mp.register_idle(idle)
mp.add_forced_key_binding("mouse_move", "image-viewer-mouse-move", function() moved = true end)
cleanup = function()
mp.remove_key_binding("image-viewer-mouse-move")
mp.unregister_idle(idle)
end
end
end
function pan_follows_cursor_handler(table)
if cleanup then
cleanup()
cleanup = nil
end
if table["event"] == "down" then
local dim = mp.get_property_native("osd-dimensions")
if not dim then return end
local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
local moved = true
local idle = function()
if moved then
local mX, mY = mp.get_mouse_pos()
local x = math.min(1, math.max(- 2 * mX / dim.w + 1, -1))
local y = math.min(1, math.max(- 2 * mY / dim.h + 1, -1))
local command = ""
local margin = opts.pan_follows_cursor_margin
if dim.ml + dim.mr < 0 then
command = command .. "no-osd set video-pan-x " .. clamp(x * (2 * margin - dim.ml - dim.mr) / (2 * video_size[1]), -3, 3) .. ";"
elseif mp.get_property_number("video-pan-x") ~= 0 then
command = command .. "no-osd set video-pan-x " .. "0;"
end
if dim.mt + dim.mb < 0 then
command = command .. "no-osd set video-pan-y " .. clamp(y * (2 * margin - dim.mt - dim.mb) / (2 * video_size[2]), -3, 3) .. ";"
elseif mp.get_property_number("video-pan-y") ~= 0 then
command = command .. "no-osd set video-pan-y " .. "0;"
end
if command ~= "" then
mp.command(command)
end
moved = false
end
end
mp.register_idle(idle)
mp.add_forced_key_binding("mouse_move", "image-viewer-mouse-move", function() moved = true end)
cleanup = function()
mp.remove_key_binding("image-viewer-mouse-move")
mp.unregister_idle(idle)
end
end
end
function cursor_centric_zoom_handler(amt)
local zoom_inc = tonumber(amt)
if not zoom_inc or zoom_inc == 0 then return end
local dim = mp.get_property_native("osd-dimensions")
if not dim then return end
local margin = opts.cursor_centric_zoom_margin
local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
-- the size in pixels of the (in|de)crement
local diff_width = (2 ^ zoom_inc - 1) * video_size[1]
local diff_height = (2 ^ zoom_inc - 1) * video_size[2]
if not opts.cursor_centric_zoom_dezoom_if_full_view and
zoom_inc < 0 and
video_size[1] + diff_width + 2 * margin <= dim.w and
video_size[2] + diff_height + 2 * margin <= dim.h
then
-- the zoom decrement is too much, reduce it such that the full image is visible, no more, no less
-- in addition, this should take care of trying too zoom out while everything is already visible
local new_zoom_inc_x = math.log((dim.w - 2 * margin) / video_size[1]) / math.log(2)
local new_zoom_inc_y = math.log((dim.h - 2 * margin) / video_size[2]) / math.log(2)
local new_zoom_inc = math.min(0, math.min(new_zoom_inc_x, new_zoom_inc_y))
zoom_inc = new_zoom_inc
diff_width = (2 ^ zoom_inc - 1) * video_size[1]
diff_height = (2 ^ zoom_inc - 1) * video_size[2]
end
local new_width = video_size[1] + diff_width
local new_height = video_size[2] + diff_height
local mouse_pos_origin = {}
mouse_pos_origin[1], mouse_pos_origin[2] = mp.get_mouse_pos()
local new_pan_x, new_pan_y
-- some additional constraints:
-- if image can be fully visible (in either direction), set pan to 0
-- if border would show on either side, then prefer adjusting the pan even if not cursor-centric
local auto_c = opts.cursor_centric_zoom_auto_center
if auto_c and video_size[1] + diff_width + 2 * margin <= dim.w then
new_pan_x = 0
else
local pan_x = mp.get_property("video-pan-x")
local rx = (dim.ml + video_size[1] / 2 - mouse_pos_origin[1]) / (video_size[1] / 2)
new_pan_x = (pan_x * video_size[1] + rx * diff_width / 2) / new_width
if auto_c then
new_pan_x = clamp(new_pan_x, (dim.w - 2 * margin) / (2 * new_width) - 0.5, - (dim.w - 2 * margin) / (2 * new_width) + 0.5)
end
end
if auto_c and video_size[2] + diff_height + 2 * margin <= dim.h then
new_pan_y = 0
else
local pan_y = mp.get_property("video-pan-y")
local ry = (dim.mt + video_size[2] / 2 - mouse_pos_origin[2]) / (video_size[2] / 2)
new_pan_y = (pan_y * video_size[2] + ry * diff_height / 2) / new_height
if auto_c then
new_pan_y = clamp(new_pan_y, (dim.h - 2 * margin) / (2 * new_height) - 0.5, - (dim.h - 2 * margin) / (2 * new_height) + 0.5)
end
end
local zoom_origin = mp.get_property("video-zoom")
mp.command("no-osd set video-zoom " .. zoom_origin + zoom_inc .. "; no-osd set video-pan-x " .. clamp(new_pan_x, -3, 3) .. "; no-osd set video-pan-y " .. clamp(new_pan_y, -3, 3))
end
function align_border(x, y)
local dim = mp.get_property_native("osd-dimensions")
if not dim then return end
local video_size = { dim.w - dim.ml - dim.mr, dim.h - dim.mt - dim.mb }
local x, y = tonumber(x), tonumber(y)
local command = ""
if x then
command = command .. "no-osd set video-pan-x " .. clamp(- x * (dim.ml + dim.mr) / (2 * video_size[1]), -3, 3) .. ";"
end
if y then
command = command .. "no-osd set video-pan-y " .. clamp(- y * (dim.mt + dim.mb) / (2 * video_size[2]), -3, 3) .. ";"
end
if command ~= "" then
mp.command(command)
end
end
function pan_image(axis, amount, zoom_invariant, image_constrained)
amount = tonumber(amount)
if not amount or amount == 0 or axis ~= "x" and axis ~= "y" then return end
if zoom_invariant == "yes" then
amount = amount / 2 ^ mp.get_property_number("video-zoom")
end
local prop = "video-pan-" .. axis
local old_pan = mp.get_property_number(prop)
if image_constrained == "yes" then
local dim = mp.get_property_native("osd-dimensions")
if not dim then return end
local margin =
(axis == "x" and amount > 0) and dim.ml
or (axis == "x" and amount < 0) and dim.mr
or (amount > 0) and dim.mt
or (amount < 0) and dim.mb
local vid_size = (axis == "x") and (dim.w - dim.ml - dim.mr) or (dim.h - dim.mt - dim.mb)
local pixels_moved = math.abs(amount) * vid_size
-- the margin is already visible, no point going further
if margin >= 0 then
return
elseif margin + pixels_moved > 0 then
amount = -(math.abs(amount) / amount) * margin / vid_size
end
end
mp.set_property_number(prop, old_pan + amount)
end
--[[
function rotate_video(amt)
local rot = mp.get_property_number("video-rotate")
rot = (rot + amt) % 360
mp.set_property_number("video-rotate", rot)
end
]]--
--[[
function reset_pan_if_visible()
local dim = mp.get_property_native("osd-dimensions")
if not dim then return end
local command = ""
if (dim.ml + dim.mr >= 0) then
command = command .. "no-osd set video-pan-x 0" .. ";"
end
if (dim.mt + dim.mb >= 0) then
command = command .. "no-osd set video-pan-y 0" .. ";"
end
if command ~= "" then
mp.command(command)
end
end
]]--
mp.add_key_binding(nil, "drag-to-pan", drag_to_pan_handler, {complex = true})
mp.add_key_binding(nil, "pan-follows-cursor", pan_follows_cursor_handler, {complex = true})
mp.add_key_binding(nil, "cursor-centric-zoom", cursor_centric_zoom_handler)
mp.add_key_binding(nil, "align-border", align_border)
mp.add_key_binding(nil, "pan-image", pan_image)
--mp.add_key_binding(nil, "rotate-video", rotate_video) --用原生功能取代
--mp.add_key_binding(nil, "reset-pan-if-visible", reset_pan_if_visible) --用原生功能取代
--mp.add_key_binding(nil, "force-print-filename", force_print_filename)
mp.set_property_native("user-data/osc/margins", { l = 0, r = 0, t = 0, b = 0.04 })
local opts = {
enabled = true,
size = 36,
margin = 5,
text_top_left = "",
text_top_right = "",
text_bottom_left = "${filename}",
text_bottom_right = "[${playlist-pos-1}/${playlist-count}][${dwidth:?}x${dheight:?}]",
}
local msg = require 'mp.msg'
local assdraw = require 'mp.assdraw'
local options = require 'mp.options'
options.read_options(opts, nil, function(c)
if c["enabled"] then
if opts.enabled then
enable()
else
disable()
end
end
if c["size"] or c["margin"] then
mark_stale()
end
if c["text_top_left"] or
c["text_top_right"] or
c["text_bottom_left"] or
c["text_bottom_right"]
then
observe_properties()
mark_stale()
end
end)
local stale = true
local active = false
function draw_ass(ass)
local ww, wh = mp.get_osd_size()
mp.set_osd_ass(ww, wh, ass)
end
function refresh()
if not stale then return end
stale = false
local a = assdraw:ass_new()
local draw_text = function(text, an, x, y)
if text == "" then return end
local expanded = mp.command_native({ "expand-text", text})
if not expanded then
msg.error("Error expanding info_ontop")
return
end
msg.verbose("info_ontop changed to: " .. expanded)
a:new_event()
a:an(an)
a:pos(x,y)
a:append("{\\fs".. opts.size.. "}{\\bord1.0}")
a:append(expanded)
end
local w,h = mp.get_osd_size()
local m = opts.margin
draw_text(opts.text_top_left, 7, m, m)
draw_text(opts.text_top_right, 9, w-m, m)
draw_text(opts.text_bottom_left, 1, m, h-m)
draw_text(opts.text_bottom_right, 3, w-m, h-m)
draw_ass(a.text)
end
function mark_stale()
stale = true
end
function observe_properties()
mp.unobserve_property(mark_stale)
if not active then return end
for _, str in ipairs({
opts.text_top_left,
opts.text_top_right,
opts.text_bottom_left,
opts.text_bottom_right,
}) do
local start = 0
while true do
local s, e, cap = string.find(str, "%${[?!]?([%l%d-/]*)", start)
if not s then break end
msg.verbose("Observing property " .. cap)
mp.observe_property(cap, nil, mark_stale)
start = e
end
end
mp.observe_property("osd-width", nil, mark_stale)
mp.observe_property("osd-height", nil, mark_stale)
end
function enable()
if active then return end
active = true
observe_properties()
mp.register_idle(refresh)
mark_stale()
end
function disable()
if not active then return end
active = false
observe_properties()
mp.unregister_idle(refresh)
draw_ass("")
end
function toggle()
if active then
disable()
else
enable()
end
end
if opts.enabled then
enable()
end
mp.add_key_binding(nil, "enable", enable)
mp.add_key_binding(nil, "disable", disable)
mp.add_key_binding(nil, "toggle", toggle)
` script-binding console/enable # 打开控制台
I script-binding display-stats-toggle # 常驻/关闭 统计数据
CTRL+h no-osd cycle-values hwdec no auto # (只用于强制更新单帧图片的统计数据)
A cycle-values video-aspect-override -1 0 # 切换 比例信息覆盖
z set video-zoom 0 ; cycle-values video-unscaled no yes # 切换 图片自动缩放
ALT+c cycle icc-profile-auto # 开/关 自动色彩管理
s screenshot video # 截图(源分辨率)
CTRL+s screenshot window # 截图(实际显示)
UP repeatable playlist-prev # 上一个图片
DOWN repeatable playlist-next # 下一个图片
LEFT frame-back-step # 上一帧
RIGHT frame-step # 下一帧
MBTN_LEFT_DBL cycle fullscreen # 鼠标左键-双击 切换全屏
WHEEL_UP script-message-to img_pos cursor-centric-zoom 0.1 # 变焦放大
WHEEL_DOWN script-message-to img_pos cursor-centric-zoom -0.1 # 变焦缩小
MBTN_LEFT script-binding img_pos/drag-to-pan # 鼠标左键-按住 拖放画布
#MBTN_RIGHT script-binding img_pos/pan-follows-cursor # 鼠标右键-按住 追踪拖放
##移动对齐图像与窗口的边界
##最后二个参数分别为:对齐方向的值( -1 和 1 为贴靠边界)
CTRL+KP8 script-message-to img_pos align-border "" -1 # 移至最上
CTRL+KP2 script-message-to img_pos align-border "" 1 # 移至最下
CTRL+KP4 script-message-to img_pos align-border 1 "" # 移至最左
CTRL+KP6 script-message-to img_pos align-border -1 "" # 移至最右
##平移图像
##最后四个参数分别为:方向|步进量|是否以固定量平移而无视变焦|如果图像超出窗口范围是否停止平移
KP8 repeatable script-message-to img_pos pan-image y +0.1 yes yes # 上移
KP2 repeatable script-message-to img_pos pan-image y -0.1 yes yes # 下移
KP4 repeatable script-message-to img_pos pan-image x +0.1 yes yes # 左移
KP6 repeatable script-message-to img_pos pan-image x -0.1 yes yes # 右移
CTRL+b set video-pan-x 0 ; set video-pan-y 0 ; set video-zoom 0 ; show-text "重置平移与变焦"
CTRL+DOWN vf toggle @flipH:hflip ; show-text "垂直翻转"
CTRL+UP vf toggle @flipV:vflip ; show-text "水平翻转"
CTRL+LEFT cycle-values video-rotate "0" "270" "180" "90" ; show-text "旋转角度:${video-rotate}"
CTRL+RIGHT cycle-values video-rotate "0" "90" "180" "270" ; show-text "旋转角度:${video-rotate}"
b set video-rotate 0 ; vf remove @flipH,@flipV ; show-text "重置旋转角度与翻转"
F1 script-binding eq/toggle # 常驻/关闭 OSD视频均衡器界面
ALT+F1 script-binding eq/reset ; show-text "重置图像均衡器"
F2 script-binding info_ontop/toggle # 常驻/关闭 自定义OSD文本置顶信息
F3 script-binding minimap/toggle # 常驻/关闭 视野位置微型图
F4 script-binding ruler/ruler ; show-text "测量尺工具"
##预览VapourSynth滤镜
ALT+1 vf toggle @VS:vapoursynth="~~/vs/SR_ESRGAN_DML.vpy" # 开/关 ESRGAN_DX12 (vsLite/vsMega)
ALT+2 vf toggle @VS:vapoursynth="~~/vs/SR_ESRGAN_NV.vpy" # 开/关 ESRGAN_RTX (vsMega)
#ALT+3
#ALT+4
#ALT+5
#ALT+6
#ALT+7
#ALT+8
#ALT+9
#ALT+0
local opts = {
enabled = true,
center = "92,92",
scale = 12,
max_size = "16,16",
image_opacity = "88",
image_color = "BBBBBB",
view_opacity = "BB",
view_color = "222222",
view_above_image = true,
hide_when_full_image_in_view = true,
}
local msg = require 'mp.msg'
local assdraw = require 'mp.assdraw'
local options = require 'mp.options'
options.read_options(opts, nil, function(c)
if c["enabled"] then
if opts.enabled then
enable()
else
disable()
end
end
mark_stale()
end)
function split_comma(input)
local ret = {}
for str in string.gmatch(input, "([^,]+)") do
ret[#ret + 1] = tonumber(str)
end
return ret
end
local active = false
local refresh = true
function draw_ass(ass)
local ww, wh = mp.get_osd_size()
mp.set_osd_ass(ww, wh, ass)
end
function mark_stale()
refresh = true
end
function refresh_minimap()
if not refresh then return end
refresh = false
local dim = mp.get_property_native("osd-dimensions")
if not dim then
draw_ass("")
return
end
local ww, wh = dim.w, dim.h
if not (ww > 0 and wh > 0) then return end
if opts.hide_when_full_image_in_view then
if dim.mt >= 0 and dim.mb >= 0 and dim.ml >= 0 and dim.mr >= 0 then
draw_ass("")
return
end
end
local center = split_comma(opts.center)
center[1] = center[1] * 0.01 * ww
center[2] = center[2] * 0.01 * wh
local cutoff = split_comma(opts.max_size)
cutoff[1] = cutoff[1] * 0.01 * ww * 0.5
cutoff[2] = cutoff[2] * 0.01 * wh * 0.5
local a = assdraw.ass_new()
local draw = function(x, y, w, h, opacity, color)
a:new_event()
a:pos(center[1], center[2])
a:append("{\\bord0}")
a:append("{\\shad0}")
a:append("{\\c&" .. color .. "&}")
a:append("{\\2a&HFF}")
a:append("{\\3a&HFF}")
a:append("{\\4a&HFF}")
a:append("{\\1a&H" .. opacity .. "}")
w = w * 0.5
h = h * 0.5
a:draw_start()
local rounded = {true,true,true,true} -- tl, tr, br, bl
local x0,y0,x1,y1 = x-w, y-h, x+w, y+h
if x0 < -cutoff[1] then
x0 = -cutoff[1]
rounded[4] = false
rounded[1] = false
end
if y0 < -cutoff[2] then
y0 = -cutoff[2]
rounded[1] = false
rounded[2] = false
end
if x1 > cutoff[1] then
x1 = cutoff[1]
rounded[2] = false
rounded[3] = false
end
if y1 > cutoff[2] then
y1 = cutoff[2]
rounded[3] = false
rounded[4] = false
end
local r = 3
local c = 0.551915024494 * r
if rounded[0] then
a:move_to(x0 + r, y0)
else
a:move_to(x0,y0)
end
if rounded[1] then
a:line_to(x1 - r, y0)
a:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r)
else
a:line_to(x1, y0)
end
if rounded[2] then
a:line_to(x1, y1 - r)
a:bezier_curve(x1, y1 - r + c, x1 - r + c, y1, x1 - r, y1)
else
a:line_to(x1, y1)
end
if rounded[3] then
a:line_to(x0 + r, y1)
a:bezier_curve(x0 + r - c, y1, x0, y1 - r + c, x0, y1 - r)
else
a:line_to(x0, y1)
end
if rounded[4] then
a:line_to(x0, y0 + r)
a:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0)
else
a:line_to(x0, y0)
end
a:draw_stop()
end
local image = function()
draw((dim.ml/2 - dim.mr/2) / opts.scale,
(dim.mt/2 - dim.mb/2) / opts.scale,
(ww - dim.ml - dim.mr) / opts.scale,
(wh - dim.mt - dim.mb) / opts.scale,
opts.image_opacity,
opts.image_color)
end
local view = function()
draw(0,
0,
ww / opts.scale,
wh / opts.scale,
opts.view_opacity,
opts.view_color)
end
if opts.view_above_image then
image()
view()
else
view()
image()
end
draw_ass(a.text)
end
function enable()
if active then return end
active = true
mp.observe_property("osd-dimensions", nil, mark_stale)
mp.register_idle(refresh_minimap)
mark_stale()
end
function disable()
if not active then return end
active = false
mp.unobserve_property(mark_stale)
mp.unregister_idle(refresh_minimap)
draw_ass("")
end
function toggle()
if active then
disable()
else
enable()
end
end
if opts.enabled then
enable()
end
mp.add_key_binding(nil, "enable", enable)
mp.add_key_binding(nil, "disable", disable)
mp.add_key_binding(nil, "toggle", toggle)

简介

ver.2023-11-25

mpv-MIV

此分支仅演示将mpv作为图片查看器的特定配置

Minimalist Image Viewer

mpv的一种衍生用法,最早只是我个人用来预览waifu2x的输出效果;
而由于mpv的部分良好特性似乎也被很多人用来当作图片查看器,于是合并两类用途就有了此分支。

该仓库没有细致的注释,默认你已基本熟悉mpv的使用。也不提供完整的成品包,只是一个作为有此类需求的样本参考。

Tip

mpv-lazy 懒人包的用户,可以快速将其转换为 mpv-MIV :
使用对应的版本(查看commit的信息),移除原包 portable_config 目录中的除 shaders 文件夹外的所有文件,再将此分支的 portable_config 覆盖,即可完成部署。

P.S.
mpv有下列缺陷(相较于常规的图片浏览器)因此只能作为一个玩具型的图片查看器 ——

  • 无增量加载且限制图像的加载体积(受限于显卡纹理)
  • 无预读
  • 无法一次性显示多张图片
  • 处理非常大的图像很弱
  • 不启用icc修正时会忽略图片的内嵌icc
  • 部分处理只应用于YUV→RGB的矩阵转换过程中

参考:
image.lua
mpv-image-viewer

############
# 核心参数 #
############
#log-file = "~~desktop/mpv-MIV.log"
include = "~~/script-opts.conf"
include = "~~/profiles.conf"
##⇘⇘显示图片时的质量与体验优化
vo = gpu
gpu-context = d3d11
hwdec-codecs = "" # 配合切换解码的hack,以强制刷新stats的数据显示
#glsl-shaders = "~~/shaders/AMD-FSR-EASU_rgb.glsl"
cscale = spline36 # 图片大多无色度半采样(大多数时候为无效参数)
scale = ewa_robidouxsharp # 可以用 nearest 算法使像素被精确复制
dscale = mitchell # 图片的原生分辨率通常符合描述信息,因此缩小时不建议使用偏锐利的算法
sigmoid-upscaling = yes
correct-downscaling = yes # 同理 --dscale ,削弱缩小时产生的锯齿感
#dither-depth = 10
#deband = yes # 去色带对图片的效果很弱
background = 0.25 # 偏灰底色的背景以区分图片的黑边
image-display-duration = inf # 图片呈现时间改为无限,默认 1 (单位为秒)
loop-file = inf # 防止动图播完暂停
loop-playlist = inf
##⇘⇘屏蔽音频与字幕类的无用项
ao = null
aid = no
mute = yes
audio-file-auto = no
sid = no
sub-auto = no
##⇘⇘把截屏当作简易的二次导出工具
screenshot-format = png # jxl
#screenshot-png-compression = 9
#screenshot-png-filter = 5
screenshot-jxl-distance = 0
#screenshot-jxl-effort = 9
#screenshot-tag-colorspace = no
#screenshot-high-bit-depth = no
#screenshot-sw = yes
##⇘⇘杂项处理
idle = yes
term-status-msg =
#msg-level = all=no
title = "${?path:${path}}${!path:---未载入文件---}"
window-dragging = no # 与预设的自由拖拽图片的快捷键功能冲突
auto-window-resize = no # 禁用窗口自动调节大小
keepaspect-window = no
input-builtin-bindings = no
osc = no # 使用第三方OSC(此模式下内置osc几乎是无用的)
icc-cache = yes
icc-cache-dir = "~~/_cache/icc"
gpu-shader-cache = yes
gpu-shader-cache-dir = "~~/_cache/shader"
watch-later-directory = "~~/_cache/watch_later"
##############
# 配置预设组 #
##############
[extension.jpg]
video-aspect-override = no # JPG的像素比例信息可能是错的(PNG格式同理)
[extension.jpeg]
profile = extension.jpg
[extension.jfif]
profile = extension.jpg
[extension.png]
video-aspect-override = no
[extension.apng]
profile = extension.png
local opts = {
show_distance = true,
show_coordinates = true,
coordinates_space = "image",
show_angles = "degrees",
line_width = 2,
dots_radius = 3,
font_size = 36,
line_color = "33",
confirm_bindings = "MBTN_LEFT,ENTER",
exit_bindings = "ESC",
set_first_point_on_begin = false,
clear_on_second_point_set = false,
}
local options = require 'mp.options'
local msg = require 'mp.msg'
local assdraw = require 'mp.assdraw'
local state = 0 -- {0,1,2,3} = {inactive,setting first point,setting second point,done}
local first_point = nil -- in normalized video space coordinates
local second_point = nil -- in normalized video space coordinates
local video_dimensions_stale = false
function split(input)
local ret = {}
for str in string.gmatch(input, "([^,]+)") do
ret[#ret + 1] = str
end
return ret
end
local confirm_bindings = split(opts.confirm_bindings)
local exit_bindings = split(opts.exit_bindings)
options.read_options(opts, nil, function()
if state ~= 0 then
remove_bindings()
end
confirm_bindings = split(opts.confirm_bindings)
exit_bindings = split(opts.exit_bindings)
if state ~= 0 then
add_bindings()
mark_stale()
end
end)
function draw_ass(ass)
local ww, wh = mp.get_osd_size()
mp.set_osd_ass(ww, wh, ass)
end
function cursor_video_space_normalized(dim)
local mx, my = mp.get_mouse_pos()
local ret = {}
ret[1] = (mx - dim.ml) / (dim.w - dim.ml - dim.mr)
ret[2] = (my - dim.mt) / (dim.h - dim.mt - dim.mb)
return ret
end
function refresh()
if not video_dimensions_stale then return end
video_dimensions_stale = false
local dim = mp.get_property_native("osd-dimensions")
local out_params = mp.get_property_native("video-out-params")
if not dim or not out_params then
draw_ass("")
return
end
local vid_width = out_params.dw
local vid_height = out_params.dh
function video_space_normalized_to_video(point)
local ret = {}
ret[1] = point[1] * vid_width
ret[2] = point[2] * vid_height
return ret
end
function video_space_normalized_to_screen(point)
local ret = {}
ret[1] = point[1] * (dim.w - dim.ml - dim.mr) + dim.ml
ret[2] = point[2] * (dim.h - dim.mt - dim.mb) + dim.mt
return ret
end
local line_start = {}
local line_end = {}
if second_point then
line_start.image = video_space_normalized_to_video(first_point)
line_start.screen = video_space_normalized_to_screen(first_point)
line_end.image = video_space_normalized_to_video(second_point)
line_end.screen = video_space_normalized_to_screen(second_point)
elseif first_point then
line_start.image = video_space_normalized_to_video(first_point)
line_start.screen = video_space_normalized_to_screen(first_point)
line_end.image = video_space_normalized_to_video(cursor_video_space_normalized(dim))
line_end.screen = {}
line_end.screen[1], line_end.screen[2] = mp.get_mouse_pos()
else
local mx, my = mp.get_mouse_pos()
line_start.image = video_space_normalized_to_video(cursor_video_space_normalized(dim))
line_start.screen = {}
line_start.screen[1], line_start.screen[2] = mp.get_mouse_pos()
line_end = line_start
end
local distinct = (math.abs(line_start.screen[1] - line_end.screen[1]) >= 1
or math.abs(line_start.screen[2] - line_end.screen[2]) >= 1)
local a = assdraw:ass_new()
local draw_setup = function(bord)
a:new_event()
a:pos(0,0)
a:append("{\\bord" .. bord .. "}")
a:append("{\\shad0}")
local r = opts.line_color
a:append("{\\3c&H".. r .. r .. r .. "&}")
a:append("{\\1a&HFF}")
a:append("{\\2a&HFF}")
a:append("{\\3a&H00}")
a:append("{\\4a&HFF}")
a:draw_start()
end
local dot = function(pos, size)
draw_setup(size)
a:move_to(pos[1], pos[2]-0.5)
a:line_to(pos[1], pos[2]+0.5)
end
local line = function(from, to, size)
draw_setup(size)
a:move_to(from[1], from[2])
a:line_to(to[1], to[2])
end
if distinct then
dot(line_start.screen, opts.dots_radius)
line(line_start.screen, line_end.screen, opts.line_width)
dot(line_end.screen, opts.dots_radius)
else
dot(line_start.screen, opts.dots_radius)
end
local line_info = function()
if not opts.show_distance then return end
a:new_event()
a:append("{\\fs36}{\\bord1}")
a:pos((line_start.screen[1] + line_end.screen[1]) / 2, (line_start.screen[2] + line_end.screen[2]) / 2)
local an = 1
if line_start.image[1] < line_end.image[1] then an = an + 2 end
if line_start.image[2] < line_end.image[2] then an = an + 6 end
a:an(an)
local image = math.sqrt(math.pow(line_start.image[1] - line_end.image[1], 2) + math.pow(line_start.image[2] - line_end.image[2], 2))
local screen = math.sqrt(math.pow(line_start.screen[1] - line_end.screen[1], 2) + math.pow(line_start.screen[2] - line_end.screen[2], 2))
if opts.coordinates_space == "both" then
a:append(string.format("image: %.1f\\Nscreen: %.1f", image, screen))
elseif opts.coordinates_space == "image" then
a:append(string.format("%.1f", image))
elseif opts.coordinates_space == "window" then
a:append(string.format("%.1f", screen))
end
end
local dot_info = function(pos, opposite)
if not opts.show_coordinates then return end
a:new_event()
a:append("{\\fs" .. opts.font_size .."}{\\bord1}")
a:pos(pos.screen[1], pos.screen[2])
local an
if distinct then
an = 1
if line_start.image[1] > line_end.image[1] then an = an + 2 end
if line_start.image[2] < line_end.image[2] then an = an + 6 end
else
an = 7
end
if opposite then
an = 9 + 1 - an
end
a:an(an)
if opts.coordinates_space == "both" then
a:append(string.format("image: %.1f, %.1f\\Nscreen: %i, %i",
pos.image[1], pos.image[2], pos.screen[1], pos.screen[2]))
elseif opts.coordinates_space == "image" then
a:append(string.format("%.1f, %.1f", pos.image[1], pos.image[2]))
elseif opts.coordinates_space == "window" then
a:append(string.format("%i, %i", pos.screen[1], pos.screen[2]))
end
end
dot_info(line_start, true)
if distinct then
line_info()
dot_info(line_end, false)
end
if distinct and opts.show_angles ~= "no" then
local dist = 50
local pos_from_angle = function(mult, angle)
return {
line_start.screen[1] + mult * dist * math.cos(angle),
line_start.screen[2] + mult * dist * math.sin(angle),
}
end
local extended = { line_start.screen[1], line_start.screen[2] }
if line_end.screen[1] > line_start.screen[1] then
extended[1] = extended[1] + dist
else
extended[1] = extended[1] - dist
end
line(line_start.screen, extended, math.max(0, opts.line_width-0.5))
local angle = math.atan(math.abs(line_start.image[2] - line_end.image[2]) / math.abs(line_start.image[1] - line_end.image[1]))
local fix_angle
local an
if line_end.image[2] < line_start.image[2] and line_end.image[1] > line_start.image[1] then
-- upper-right
an = 4
fix_angle = function(angle) return - angle end
elseif line_end.image[2] < line_start.image[2] then
-- upper-left
an = 6
fix_angle = function(angle) return math.pi + angle end
elseif line_end.image[1] < line_start.image[1] then
-- bottom-left
an = 6
fix_angle = function(angle) return math.pi - angle end
else
-- bottom-right
an = 4
fix_angle = function(angle) return angle end
end
-- should implement this https://math.stackexchange.com/questions/873224/calculate-control-points-of-cubic-bezier-curve-approximating-a-part-of-a-circle
local cp1 = pos_from_angle(1, fix_angle(angle*1/4))
local cp2 = pos_from_angle(1, fix_angle(angle*3/4))
local p2 = pos_from_angle(1, fix_angle(angle))
a:bezier_curve(cp1[1], cp1[2], cp2[1], cp2[2], p2[1], p2[2])
a:new_event()
a:append("{\\fs" .. opts.font_size .."}{\\bord1}")
local text_pos = pos_from_angle(1.1, fix_angle(angle*2/3)) -- you'd think /2 would make more sense, but *2/3 looks better
a:pos(text_pos[1], text_pos[2])
a:an(an)
if opts.show_angles == "both" then
a:append(string.format("%.2f\\N%.1f°", angle, angle / math.pi * 180))
elseif opts.show_angles == "degrees" then
a:append(string.format("%.1f°", angle / math.pi * 180))
elseif opts.show_angles == "radians" then
a:append(string.format("%.2f", angle))
end
end
draw_ass(a.text)
end
function mark_stale()
video_dimensions_stale = true
end
function add_bindings()
mp.add_forced_key_binding("mouse_move", "ruler-mouse-move", mark_stale)
for _, key in ipairs(confirm_bindings) do
mp.add_forced_key_binding(key, "ruler-next-" .. key, next_step)
end
for _, key in ipairs(exit_bindings) do
mp.add_forced_key_binding(key, "ruler-stop-" .. key, stop)
end
end
function remove_bindings()
for _, key in ipairs(confirm_bindings) do
mp.remove_key_binding("ruler-next-" .. key)
end
for _, key in ipairs(exit_bindings) do
mp.remove_key_binding("ruler-stop-" .. key)
end
mp.remove_key_binding("ruler-mouse-move")
end
function next_step()
if state == 0 then
state = 1
mp.register_idle(refresh)
mp.observe_property("osd-dimensions", nil, mark_stale)
mark_stale()
add_bindings()
if opts.set_first_point_on_begin then
next_step()
end
elseif state == 1 then
local dim = mp.get_property_native("osd-dimensions")
if not dim then return end
state = 2
first_point = cursor_video_space_normalized(dim)
elseif state == 2 then
local dim = mp.get_property_native("osd-dimensions")
if not dim then return end
state = 3
second_point = cursor_video_space_normalized(dim)
if opts.clear_on_second_point_set then
next_step()
end
else
stop()
end
end
function stop()
if state == 0 then return end
mp.unregister_idle(refresh)
mp.unobserve_property(mark_stale)
remove_bindings()
state = 0
first_point = nil
second_point = nil
draw_ass("")
end
mp.add_key_binding(nil, "ruler", next_step)
##############
# 脚本参数集 #
##############
##⇘⇘图像的移动与变焦
script-opts-append = img_pos-drag_to_pan_margin=0 # 拖放画布时的边距,默认 50
script-opts-append = img_pos-drag_to_pan_move_if_full_view=yes # 图片显示全部时是否允许拖放画布,默认 no
script-opts-append = img_pos-pan_follows_cursor_margin=0 # 追踪拖放时的边距,默认 50
script-opts-append = img_pos-cursor_centric_zoom_margin=45 # 变焦缩放时的边距,默认 50
#script-opts-append = img_pos-cursor_centric_zoom_auto_center=yes # 图片显示全部时,使用变焦缩放后是否居中图像,默认 yes
#script-opts-append = img_pos-cursor_centric_zoom_dezoom_if_full_view=no # 图片显示全部时,是否允许无限变焦缩小,默认 no
##⇘⇘置顶的自定义OSD信息
#script-opts-append = info_ontop-enabled=no # 是否显示置顶OSD信息,默认 yes
script-opts-append = info_ontop-size=32 # 字体大小,默认 36
script-opts-append = info_ontop-margin=4 # 文本边距,默认 5
##自定义文本显示,示例即默认值
##支持换行符 \N 与属性扩展以及ASS标签 https://aeg-dev.github.io/AegiSite/docs/3.2/ass_tags/
#script-opts-append = info_ontop-text_top_left=
#script-opts-append = info_ontop-text_top_right=
#script-opts-append = info_ontop-text_bottom_left=${filename}
#script-opts-append = info_ontop-text_bottom_right=[${playlist-pos-1}/${playlist-count}][${dwidth:?}x${dheight:?}]
##⇘⇘微型比例图
#script-opts-append = minimap-enabled=no # 启用位置预览的微型图,默认 yes
script-opts-append = minimap-center=90,10 # 微型图中央的位置,以窗口的百分比为单位,默认 92,92
script-opts-append = minimap-scale=20 # 微型图的比例,默认 12
script-opts-append = minimap-max_size=18,18 # 微型图的截断尺寸,默认 16,16
##“图像部分”与“可视部分”的透明度和颜色,示例即默认值
#script-opts-append = minimap-image_opacity=88
#script-opts-append = minimap-image_color=BBBBBB
#script-opts-append = minimap-view_opacity=BB
#script-opts-append = minimap-view_color=222222
#script-opts-append = minimap-view_above_image=yes # 微型图是否置于图像上方,默认 yes
#script-opts-append = minimap-hide_when_full_image_in_view=no # “可视部分”≥“图像部分”时,是否隐藏微型图,默认 yes
##⇘⇘标尺工具
#script-opts-append = ruler-show_distance=no # 是否显示两点之间的线的长度,默认 yes
#script-opts-append = ruler-show_coordinates=no # 是否显示两点的坐标,默认 yes
#script-opts-append = ruler-coordinates_space=both # <image|window|both> 选择显示的坐标空间,默认 image (仅图像)
#script-opts-append = ruler-show_angles=both # <degrees|radians|both|no> 选择显示的角单位,默认 degrees (角度制)
##线的粗细,点的尺寸,字体大小,线的颜色 <00---FF> 。示例即默认值
#script-opts-append = ruler-line_width=2
#script-opts-append = ruler-dots_radius=3
#script-opts-append = ruler-font_size=36
#script-opts-append = ruler-line_color=33
##确定点的动态按键绑定,多个按键之间用半角逗号分离。示例即默认值
#script-opts-append = ruler-confirm_bindings=MBTN_LEFT,ENTER
#script-opts-append = ruler-exit_bindings=ESC
#script-opts-append = ruler-set_first_point_on_begin=yes # 调用标尺时是否立即确定第一个点,默认 no
#script-opts-append = ruler-clear_on_second_point_set=yes # 确定第二个点时,是否立即清除标尺,默认 no
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment