Files
yazi-gruvbox/preview/plugins/pdf-hidpi.yazi/main.lua
T
thomas.koppandClaude Opus 5 de59a5a904 feat: add preview tuning — hi-DPI PDF previewer and opener setup
preview/ holds a yazi.toml and a pdf-hidpi plugin that fix unreadable PDF
previews. Yazi calls pdftoppm without -r (150 DPI) and then caps the image at
600x900, so pages are downscaled and upscaled again; the plugin renders at
300 DPI as PNG and the cap goes to 1600x2400, turning that into supersampling.

Previewer and preloader both have to be overridden — they share the cache file.

README documents this plus what video previews can and cannot do: the preview
is a single ffmpeg keyframe by design, J/K scrubs it, and the opener offers
mpv --vo=kitty with shared-memory transfer for inline playback.

Targets the 26.9 plugin API (fs.cha, not main's fs.stat).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 20:12:42 +02:00

74 lines
2.0 KiB
Lua

-- pdf-hidpi: like Yazi's built-in `pdf` previewer, but renders at a higher
-- resolution and as PNG instead of JPEG.
--
-- The built-in one calls `pdftoppm` without `-r`, so pages come out at
-- poppler's 150 DPI default (A4 = 1240x1754) and are then JPEG-compressed.
-- On a large or a portrait pane that is not enough pixels for body text, and
-- JPEG ringing around glyphs makes it worse.
--
-- Rendering above the target size and letting `image_precache` downscale gives
-- a supersampled, much sharper result. PNG keeps glyph edges clean.
--
-- Raise DPI for even sharper text at the cost of render time and cache size.
local DPI = 300
local M = {}
function M:peek(job)
local start, cache = os.clock(), ya.file_cache(job)
if not cache then
return
end
local ok, err, bound = self:preload(job)
if bound and bound > 0 then
return ya.emit("peek", { bound - 1, only_if = job.file.url, upper_bound = true })
elseif not ok or err then
return ya.preview_widget(job, err)
end
ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock()))
local _, err = ya.image_show(cache, job.area)
ya.preview_widget(job, err)
end
function M:seek(job)
local h = cx.active.current.hovered
if h and h.url == job.file.url then
local step = ya.clamp(-1, job.units, 1)
ya.emit("peek", { math.max(0, cx.active.preview.skip + step), only_if = job.file.url })
end
end
function M:preload(job)
local cache = ya.file_cache(job)
if not cache or fs.cha(cache) then
return true
end
-- stylua: ignore
local output, err = Command("pdftoppm")
:arg({
"-f", job.skip + 1,
"-l", job.skip + 1,
"-singlefile",
"-r", DPI,
"-png",
tostring(job.file.path), tostring(cache),
})
:output()
if not output then
return true, Err("Failed to start `pdftoppm`, error: %s", err)
elseif not output.status.success then
local pages = job.skip > 0 and tonumber(output.stderr:match("the last page %((%d+)%)"))
return true, Err("Failed to convert PDF to image, stderr: %s", output.stderr), pages
end
return ya.image_precache(Url(cache .. ".png"), cache)
end
return M