├── Makefile ├── README.md ├── history-of-ball-bearings.pdf ├── pdf_hook-worker.lua └── pdf_hook.lua /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | cp pdf_hook.lua ~/.config/mpv/scripts/ 3 | cp pdf_hook-worker.lua ~/.config/mpv/scripts/pdf_hook-worker-1.lua 4 | cp pdf_hook-worker.lua ~/.config/mpv/scripts/pdf_hook-worker-2.lua 5 | 6 | test: 7 | mpv history-of-ball-bearings.pdf 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mpv-pdf 2 | A script for the MPV media player that allows you to view PDFs by way of [ImageMagick](https://www.imagemagick.org/). To improve load times and keep mpv responsive, pages are pre-rendered asynchronously and *by default*, only two pages ahead of the current page are pre-rendered. 3 | 4 | ## Dependancies 5 | - MacOS, Linux, \*BSD, etc **(Windows is NOT currently supported, but PRs are welcome)** 6 | - [ImageMagick](https://www.imagemagick.org/) 7 | - [pdfinfo](https://linux.die.net/man/1/pdfinfo) 8 | 9 | *(Install ImageMagick and `pdfinfo` using your package manager. `convert`, provided by ImageMagick, should be in your $PATH.)* 10 | 11 | 12 | ## Installation 13 | make install 14 | or 15 | 16 | cp pdf_hook.lua ~/.config/mpv/scripts/ 17 | cp pdf_hook-worker.lua ~/.config/mpv/scripts/pdf_hook-worker-1.lua 18 | cp pdf_hook-worker.lua ~/.config/mpv/scripts/pdf_hook-worker-2.lua 19 | [...] 20 | 21 | **NOTE:** 22 | 23 | **For every additional copy of pdf\_hook-worker.lua you copy into ~/.config/mpv/scripts, *mpv-pdf* will render an additional page ahead of the current page. E.g. 10 copies of pdf\_hook-worker.lua will have *mpv-pdf* render 10 pages ahead.** 24 | 25 | ## Recommended Configuration 26 | 27 | It's highly recommended that *mpv-pdf* be used in conjunction with [mpv-image-viewer](https://github.com/occivink/mpv-image-viewer) or a similar userscript. This will allow panning, zooming, etc of PDF pages (which are displayed through mpv as jpgs.) 28 | 29 | ## Technical Details 30 | *mpv-pdf* displays a PDF page by first using ImageMagick's `convert` to render it as a jpg. 31 | 32 | It accomplishes this by first reading the number of pages from the pdf using `pdfinfo`, then for each page it generates a playlist entry of the form `pdf://path/to/your.pdf[page]`. These pdf page playlist entries are also handled by pdf\_hook.lua, which dispatches an asynchronous rendering task for each to one of the pdf\_hook-worker's. To keep things responsive and to avoid rendering pages unnecessarily, an async rendering task is dispatched for the current page, and an additional rendering task is dispatched for each additional pdf\_hook-worker script you've installed. 33 | 34 | Until an asynchronous task returns, a placeholder image is shown. This code is [subject to future change](https://github.com/libass/libass) but currently the placeholder image is generated using ImageMagick as a blank white jpg with the pixel dimensions of the PDF, as computed from `pdfinfo`. 35 | 36 | jpgs produced by *mpv-pdf* are located in `/tmp/mpv-pdf/`. If your OS doesn't periodically clean `/tmp/`, this could get large... 37 | 38 | ## Plans for Future Enhancement 39 | - [x] ~~Asynchronous generation of pages~~ 40 | - [ ] Use LibASS to generate the page placeholder 41 | - [ ] Spawn asynchronous sub-scripts programmatically 42 | - [ ] Use pandoc to support more file formats (e.g. docx) 43 | - [ ] Support text search of the PDF 44 | - [ ] Use text-to-speech to generate sound files for each page. 45 | - [ ] Re-render PDF pages at different DPI's depending on the zoom setting. 46 | -------------------------------------------------------------------------------- /history-of-ball-bearings.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jgreco/mpv-pdf/ffccf76984452ad831d14db04c8711a58c4ef384/history-of-ball-bearings.pdf -------------------------------------------------------------------------------- /pdf_hook-worker.lua: -------------------------------------------------------------------------------- 1 | local utils = require 'mp.utils' 2 | local msg = require 'mp.msg' 3 | 4 | local function exec(args) 5 | local ret = utils.subprocess({args = args, cancellable=false}) 6 | return ret.status, ret.stdout, ret, ret.killed_by_us 7 | end 8 | 9 | mp.register_script_message("generate-pdf-page", function(url, density, quality) 10 | local input = string.gsub(url, "pdf://", "") 11 | local output = "/tmp/mpv-pdf/" .. string.gsub(input, "/", "|")..".jpg" 12 | exec({"mkdir", "-p", "/tmp/mpv-pdf/"}) --TODO make tmp directory configurable 13 | 14 | --convert pdf page to jpg 15 | stat,out,ret,killed = exec({"convert", 16 | "-density", density, --PPI 17 | "-quality", quality, -- jpg compression quality 18 | input, output}) 19 | 20 | mp.commandv("script-message", "pdf-page-generator-return", tostring(killed or stat ~= 0), url, output ) 21 | end) 22 | 23 | mp.commandv("script-message", "pdf-page-generator-broadcast", mp.get_script_name()) 24 | -------------------------------------------------------------------------------- /pdf_hook.lua: -------------------------------------------------------------------------------- 1 | -- pdf_hook.lua 2 | -- 3 | -- view PDFs in mpv by using ImageMagick to convert PDF pages to images 4 | -- 5 | -- Dependancies: 6 | -- * Linux / Unix / OSX (windows support should be possible, but I can't test it.) 7 | -- * ImageMagick (`convert` must be in the PATH) 8 | -- * pdfinfo 9 | -- 10 | -- Notes: jpegs are generated for each page and are placed in /tmp/mpv-pdf/. 11 | -- If your OS doesn't periodically clean /tmp/, this could get large... 12 | -- 13 | -- Use of an mpv-image-viewer is recommended for panning/zooming. 14 | -- 15 | -- Is this userscript a weird joke? To be honest I'm not really sure anymore. 16 | 17 | local utils = require 'mp.utils' 18 | local msg = require 'mp.msg' 19 | 20 | local opts = { 21 | --TODO use pandoc to support more file formats? 22 | density=150, 23 | quality=50, 24 | supported_extensions=[[ 25 | ["pdf"] 26 | ]] 27 | } 28 | (require 'mp.options').read_options(opts) 29 | opts.supported_extensions = utils.parse_json(opts.supported_extensions) 30 | 31 | local function exec(args) 32 | local ret = utils.subprocess({args = args}) 33 | return ret.status, ret.stdout, ret, ret.killed_by_us 34 | end 35 | 36 | local function findl(str, patterns) 37 | for i,p in pairs(patterns) do 38 | if str:find("%."..p.."$") then 39 | return true 40 | end 41 | end 42 | return false 43 | end 44 | 45 | generators = {} 46 | mp.register_script_message("pdf-page-generator-broadcast", function(generator_name) 47 | for _, g in ipairs(generators) do 48 | if generator_name == g then return end 49 | end 50 | generators[#generators + 1] = generator_name 51 | end) 52 | 53 | mp.register_script_message("pdf-page-generator-return", function(failed, from, to) 54 | if failed == "true" then 55 | msg.error("generator was killed..: "..from .. " to: " .. to) 56 | outstanding_tasks[from] = nil 57 | return 58 | end 59 | completed_tasks[from] = to; 60 | outstanding_tasks[from] = nil 61 | 62 | if mp.get_property("playlist/"..mp.get_property("playlist-pos").."/filename") == from then 63 | -- append new jpg to playlist, reorder it, then delete the current playlist entry 64 | mp.commandv("loadfile", to, "append") 65 | mp.commandv("playlist-move", mp.get_property("playlist-count")-1, mp.get_property("playlist-pos")+1) 66 | mp.commandv("playlist-remove", mp.get_property("playlist-pos")) 67 | end 68 | end) 69 | 70 | placeholder=nil 71 | next_generator=1 72 | outstanding_tasks={} 73 | completed_tasks={} 74 | local function request_page(url) 75 | if completed_tasks[url] then return completed_tasks[url] end 76 | if outstanding_tasks[url] then return placeholder end 77 | 78 | 79 | mp.commandv("script-message-to", generators[next_generator], "generate-pdf-page", url, tostring(opts.density), tostring(opts.quality)) 80 | outstanding_tasks[url] = generators[next_generator] 81 | 82 | next_generator = next_generator + 1 83 | if next_generator > #generators then next_generator = 1 end 84 | return placeholder 85 | end 86 | 87 | local function prefetch_pages() 88 | local urls = {} 89 | 90 | for i=mp.get_property("playlist-pos"), mp.get_property("playlist-count")-1,1 do 91 | url = mp.get_property("playlist/"..i.."/filename") 92 | if url:find("pdf://") == 1 then 93 | urls[#urls+1] = url 94 | end 95 | end 96 | 97 | for i=1,math.min(#generators,#urls),1 do 98 | request_page(urls[i]) 99 | end 100 | end 101 | 102 | mp.add_hook("on_load", 10, function () 103 | local url = mp.get_property("stream-open-filename", "") 104 | msg.debug("stream-open-filename: "..url) 105 | 106 | if (url:find("pdf://") == 1) then 107 | mp.set_property("stream-open-filename", request_page(url)) --swap in jpg (or placeholder) 108 | prefetch_pages() 109 | return 110 | end 111 | 112 | 113 | if (findl(url, opts.supported_extensions) == false) then 114 | msg.debug("did not find a supported file") 115 | return 116 | end 117 | 118 | -- get pagecount 119 | local pdfinfo = "pdfinfo" 120 | local stat,out = exec({pdfinfo, url}) 121 | local num_pages = string.match(out, "Pages:%s+(%d+)") 122 | local page_size_x = string.match(out, "Page size:%s+(%d+.*%d*) x %d+.*%d*%s+pts") / 72 * opts.density 123 | local page_size_y = string.match(out, "Page size:%s+%d+.*%d* x (%d+.*%d*)%s+pts") / 72 * opts.density 124 | local size_str = tostring(page_size_x).."x"..tostring(page_size_y) 125 | 126 | placeholder="/tmp/mpv-pdf/placeholder-"..size_str..".jpg" 127 | exec({"convert", 128 | "-size", size_str, 129 | "canvas:white", 130 | placeholder}) 131 | 132 | --build pdf:// playlist 133 | local playlist = {"#EXTM3U"} 134 | for i=0,num_pages-1,1 do 135 | table.insert(playlist, "#EXTINF:0,Page "..i) --TODO use 'real' page numbers e.g. 'ix', 'Cover', etc 136 | table.insert(playlist, "pdf://"..url.."["..i.."]") --playlist entry has the page number on it, used by `convert` 137 | end 138 | 139 | --load playlist 140 | if #playlist > 0 then 141 | mp.set_property("stream-open-filename", "memory://" .. table.concat(playlist, "\n")) 142 | end 143 | 144 | return 145 | end) 146 | --------------------------------------------------------------------------------