You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
koreader/plugins/coverimage.koplugin/main.lua

509 lines
25 KiB
Lua

-- plugin for saving a cover image to a file and scale it to fit the screen
local Device = require("device")
if not (Device.isAndroid() or Device.isEmulator() or Device.isRemarkable() or Device.isPocketBook()) then
return { disabled = true }
end
local Blitbuffer = require("ffi/blitbuffer")
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local RenderImage = require("ui/renderimage")
local ffiutil = require("ffi/util")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
local T = require("ffi/util").template
local function pathOk(filename)
local path, name = util.splitFilePathName(filename)
if not Device:isValidPath(path) then -- isValidPath expects a trailing slash
return false, T(_("Path \"%1\" isn't in a writable location."), path)
elseif not util.pathExists(path:gsub("/$", "")) then -- pathExists expects no trailing slash
return false, T(_("The path \"%1\" doesn't exist."), path)
elseif name == "" then
return false, _("Please enter a filename at the end of the path.")
elseif lfs.attributes(filename, "mode") == "directory" then
return false, T(_("The path \"%1\" must point to a file, but it points to a directory."), filename)
end
return true
end
local function getExtension(filename)
local _, name = util.splitFilePathName(filename)
return util.getFileNameSuffix(name):lower()
end
local CoverImage = WidgetContainer:new{
name = "coverimage",
is_doc_only = true,
}
function CoverImage:init()
self.cover_image_path = G_reader_settings:readSetting("cover_image_path") or "cover.jpg"
self.cover_image_format = G_reader_settings:readSetting("cover_image_format") or "auto"
self.cover_image_extension = getExtension(self.cover_image_path)
self.cover_image_quality = G_reader_settings:readSetting("cover_image_quality") or 75
self.cover_image_stretch_limit = G_reader_settings:readSetting("cover_image_stretch_limit") or 5
self.cover_image_background = G_reader_settings:readSetting("cover_image_background") or "black"
self.cover_image_fallback_path = G_reader_settings:readSetting("cover_image_fallback_path") or "cover_fallback.png"
self.enabled = G_reader_settings:isTrue("cover_image_enabled")
self.fallback = G_reader_settings:isTrue("cover_image_fallback")
self.ui.menu:registerToMainMenu(self)
end
function CoverImage:_enabled()
return self.enabled
end
function CoverImage:_fallback()
return self.fallback
end
function CoverImage:cleanUpImage()
if self.cover_image_fallback_path == "" or not self.fallback then
os.remove(self.cover_image_path)
elseif lfs.attributes(self.cover_image_fallback_path, "mode") ~= "file" then
UIManager:show(InfoMessage:new{
text = T(_("\"%1\" \nis not a valid image file!\nA valid fallback image is required in Cover-Image."), self.cover_image_fallback_path),
show_icon = true,
timeout = 10,
})
os.remove(self.cover_image_path)
elseif pathOk(self.cover_image_path) then
ffiutil.copyFile(self.cover_image_fallback_path, self.cover_image_path)
end
end
function CoverImage:createCoverImage(doc_settings)
if self.enabled and doc_settings:nilOrFalse("exclude_cover_image") then
local cover_image = self.ui.document:getCoverPageImage()
if cover_image then
local s_w, s_h = Device.screen:getWidth(), Device.screen:getHeight()
local i_w, i_h = cover_image:getWidth(), cover_image:getHeight()
local scale_factor = math.min(s_w / i_w, s_h / i_h)
if self.cover_image_background == "none" or scale_factor == 1 then
local act_format = self.cover_image_format == "auto" and self.cover_image_extension or self.cover_image_format
if not cover_image:writeToFile(self.cover_image_path, act_format, self.cover_image_quality) then
UIManager:show(InfoMessage:new{
text = _("Error writing file") .. "\n" .. self.cover_image_path,
show_icon = true,
})
end
cover_image:free()
return
end
local screen_ratio = s_w / s_h
local image_ratio = i_w / i_h
local ratio_divergence_percent = math.abs(100 - image_ratio / screen_ratio * 100)
logger.dbg("CoverImage: geometries screen=" .. screen_ratio .. ", image=" .. image_ratio .. "; ratio=" .. ratio_divergence_percent)
local image
if ratio_divergence_percent < self.cover_image_stretch_limit then -- stretch
logger.dbg("CoverImage: stretch to fullscreen")
image = RenderImage:scaleBlitBuffer(cover_image, s_w, s_h)
else -- scale
local scaled_w, scaled_h = math.floor(i_w * scale_factor), math.floor(i_h * scale_factor)
logger.dbg("CoverImage: scale to fullscreen, fill background")
cover_image = RenderImage:scaleBlitBuffer(cover_image, scaled_w, scaled_h)
-- new buffer with screen dimensions,
image = Blitbuffer.new(s_w, s_h, cover_image:getType()) -- new buffer, filled with black
if self.cover_image_background == "white" then
image:fill(Blitbuffer.COLOR_WHITE)
elseif self.cover_image_background == "gray" then
image:fill(Blitbuffer.COLOR_GRAY)
end
-- copy scaled image to buffer
if s_w > scaled_w then -- move right
image:blitFrom(cover_image, math.floor((s_w - scaled_w) / 2), 0, 0, 0, scaled_w, scaled_h)
else -- move down
image:blitFrom(cover_image, 0, math.floor((s_h - scaled_h) / 2), 0, 0, scaled_w, scaled_h)
end
end
cover_image:free()
local act_format = self.cover_image_format == "auto" and self.cover_image_extension or self.cover_image_format
if not image:writeToFile(self.cover_image_path, act_format, self.cover_image_quality) then
UIManager:show(InfoMessage:new{
text = _("Error writing file") .. "\n" .. self.cover_image_path,
show_icon = true,
})
end
image:free()
logger.dbg("CoverImage: image written to " .. self.cover_image_path)
end
end
end
function CoverImage:onCloseDocument()
logger.dbg("CoverImage: onCloseDocument")
if self.fallback then
self:cleanUpImage()
end
end
function CoverImage:onReaderReady(doc_settings)
logger.dbg("CoverImage: onReaderReady")
self:createCoverImage(doc_settings)
end
local about_text = _([[
This plugin saves the current book cover to a file. That file can be used as a screensaver on certain Android devices, such as Tolinos.
If enabled, the cover image of the actual file is stored in the selected screensaver file. Books can be excluded if desired.
If fallback is activated, the fallback file will be copied to the screensaver file on book closing.
If the filename is empty or the file doesn't exist, the cover file will be deleted and the system screensaver will be used instead.
If the fallback image isn't activated, the screensaver image will stay in place after closing a book.]])
function CoverImage:addToMainMenu(menu_items)
menu_items.coverimage = {
sorting_hint = "screen",
text = _("Cover image"),
checked_func = function()
return self.enabled or self.fallback
end,
sub_item_table = {
-- menu entry: about cover image
{
text = _("About cover image"),
keep_menu_open = true,
callback = function()
UIManager:show(InfoMessage:new{
text = about_text,
})
end,
separator = true,
},
-- menu entry: filename dialog
{
text = _("Set image path"),
checked_func = function()
return self.cover_image_path ~= "" and pathOk(self.cover_image_path)
end,
help_text = _("The cover of the current book will be stored in this file."),
keep_menu_open = true,
callback = function(menu)
local InputDialog = require("ui/widget/inputdialog")
local sample_input
sample_input = InputDialog:new{
title = _("Screensaver image filename"),
input = self.cover_image_path,
input_type = "string",
description = _("You can enter the filename of the cover image here."),
buttons = {
{
{
text = _("Cancel"),
callback = function()
UIManager:close(sample_input)
end,
},
{
text = _("Save"),
is_enter_default = true,
callback = function()
local new_cover_image_path = sample_input:getInputText()
if new_cover_image_path ~= self.cover_image_path then
self:cleanUpImage() -- with old filename
self.cover_image_path = new_cover_image_path -- update filename
G_reader_settings:saveSetting("cover_image_path", self.cover_image_path)
local is_path_ok, is_path_ok_message = pathOk(self.cover_image_path)
if self.cover_image_path ~= "" and is_path_ok then
self:createCoverImage(self.ui.doc_settings) -- with new filename
else
self.enabled = false
UIManager:show(InfoMessage:new{
text = is_path_ok_message,
show_icon = true,
})
end
end
self.cover_image_extension = getExtension(self.cover_image_path)
UIManager:close(sample_input)
menu:updateItems()
end,
},
}
},
}
UIManager:show(sample_input)
sample_input:onShowKeyboard()
end,
},
-- menu entry: enable
{
text = _("Save cover image"),
checked_func = function()
return self:_enabled() and pathOk(self.cover_image_path)
end,
enabled_func = function()
return self.cover_image_path ~= "" and pathOk(self.cover_image_path)
end,
callback = function()
if self.cover_image_path ~= "" then
self.enabled = not self.enabled
G_reader_settings:saveSetting("cover_image_enabled", self.enabled)
if self.enabled then
self:createCoverImage(self.ui.doc_settings)
else
self:cleanUpImage()
end
end
end,
},
-- menu entry: scale book cover
{
text = _("Size, background and format"),
enabled_func = function()
return self.enabled
end,
sub_item_table = {
{
text_func = function()
return T(_("Aspect ratio stretch threshold (%1%)"), self.cover_image_stretch_limit )
end,
help_text_func = function()
return T(_("If the image and the screen have a similar aspect ratio (±%1%), stretch the image instead of keeping its aspect ratio."), self.cover_image_stretch_limit )
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
local old_stretch_limit = self.cover_image_stretch_limit
local SpinWidget = require("ui/widget/spinwidget")
local size_spinner = SpinWidget:new{
width = math.floor(Device.screen:getWidth() * 0.6),
value = old_stretch_limit,
value_min = 0,
value_max = 25,
default_value = 5,
title_text = _("Set stretch threshold"),
ok_text = _("Set"),
callback = function(spin)
if self.enabled and spin.value ~= old_stretch_limit then
self.cover_image_stretch_limit = spin.value
G_reader_settings:saveSetting("cover_image_stretch_limit", self.cover_image_stretch_limit)
self:createCoverImage(self.ui.doc_settings)
end
if touchmenu_instance then touchmenu_instance:updateItems() end
end
}
UIManager:show(size_spinner)
if self.enabled and old_stretch_limit ~= self.cover_image_stretch_limit then
self:createCoverImage(self.ui.doc_settings)
end
end,
},
{
text = _("Fit to screen, black background"),
checked_func = function()
return self.cover_image_background == "black"
end,
callback = function()
local old_background = self.cover_image_background
self.cover_image_background = "black"
G_reader_settings:saveSetting("cover_image_background", self.cover_image_background)
if self.enabled and old_background ~= self.cover_image_background then
self:createCoverImage(self.ui.doc_settings)
end
end,
},
{
text = _("Fit to screen, white background"),
checked_func = function()
return self.cover_image_background == "white"
end,
callback = function()
local old_background = self.cover_image_background
self.cover_image_background = "white"
G_reader_settings:saveSetting("cover_image_background", self.cover_image_background)
if self.enabled and old_background ~= self.cover_image_background then
self:createCoverImage(self.ui.doc_settings)
end
end,
},
{
text = _("Fit to screen, gray background"),
checked_func = function()
return self.cover_image_background == "gray"
end,
callback = function()
local old_background = self.cover_image_background
self.cover_image_background = "gray"
G_reader_settings:saveSetting("cover_image_background", self.cover_image_background)
if self.enabled and old_background ~= self.cover_image_background then
self:createCoverImage(self.ui.doc_settings)
end
end,
},
{
text = _("Original image"),
checked_func = function()
return self.cover_image_background == "none"
end,
callback = function()
local old_background = self.cover_image_background
self.cover_image_background = "none"
G_reader_settings:saveSetting("cover_image_background", self.cover_image_background)
if self.enabled and old_background ~= self.cover_image_background then
self:createCoverImage(self.ui.doc_settings)
end
end,
separator = true,
},
-- menu entries: File format
{
text = _("File format derived from filename"),
help_text = _("If the file format is not supported, then JPG will be used."),
checked_func = function()
return self.cover_image_format == "auto"
end,
callback = function()
local old_cover_image_format = self.cover_image_format
self.cover_image_format = "auto"
G_reader_settings:saveSetting("cover_image_format", self.cover_image_format)
if self.enabled and old_cover_image_format ~= self.cover_image_format then
self:createCoverImage(self.ui.doc_settings)
end
end,
},
{
text = _("JPG file format"),
checked_func = function()
return self.cover_image_format == "jpg"
end,
callback = function()
local old_cover_image_format = self.cover_image_format
self.cover_image_format = "jpg"
G_reader_settings:saveSetting("cover_image_format", self.cover_image_format)
if self.enabled and old_cover_image_format ~= self.cover_image_format then
self:createCoverImage(self.ui.doc_settings)
end
end,
},
{
text = _("PNG file format"),
checked_func = function()
return self.cover_image_format == "png"
end,
callback = function()
local old_cover_image_format = self.cover_image_format
self.cover_image_format = "png"
G_reader_settings:saveSetting("cover_image_format", self.cover_image_format)
if self.enabled and old_cover_image_format ~= self.cover_image_format then
self:createCoverImage(self.ui.doc_settings)
end
end,
},
{
text = _("BMP file format"),
checked_func = function()
return self.cover_image_format == "bmp"
end,
callback = function()
local old_cover_image_format = self.cover_image_format
self.cover_image_format = "bmp"
G_reader_settings:saveSetting("cover_image_format", self.cover_image_format)
if self.enabled and old_cover_image_format ~= self.cover_image_format then
self:createCoverImage(self.ui.doc_settings)
end
end,
},
}
},
-- menu entry: exclude this cover
{
text = _("Exclude this book cover"),
checked_func = function()
return self.ui and self.ui.doc_settings and self.ui.doc_settings:isTrue("exclude_cover_image")
end,
callback = function()
if self.ui.doc_settings:isTrue("exclude_cover_image") then
self.ui.doc_settings:makeFalse("exclude_cover_image")
self:createCoverImage(self.ui.doc_settings)
else
self.ui.doc_settings:makeTrue("exclude_cover_image")
self:cleanUpImage()
end
self.ui:saveSettings()
end,
separator = true,
},
-- menu entry: set fallback image
{
text = _("Set fallback image path"),
checked_func = function()
return lfs.attributes(self.cover_image_fallback_path, "mode") == "file"
end,
keep_menu_open = true,
callback = function(menu)
local InputDialog = require("ui/widget/inputdialog")
local sample_input
sample_input = InputDialog:new{
title = _("Fallback image filename"),
input = self.cover_image_fallback_path,
input_type = "string",
description = _("Leave this empty to remove the cover when the document is closed."),
buttons = {
{
{
text = _("Cancel"),
callback = function()
UIManager:close(sample_input)
end,
},
{
text = _("Save"),
is_enter_default = true,
callback = function()
self.cover_image_fallback_path = sample_input:getInputText()
G_reader_settings:saveSetting("cover_image_fallback_path", self.cover_image_fallback_path)
if lfs.attributes(self.cover_image_fallback_path, "mode") ~= "file"
and self.cover_image_fallback_path ~= "" then
UIManager:show(InfoMessage:new{
text = T(_("\"%1\" \nis not a valid image file!\nA valid fallback image is required in Cover-Image"),
self.cover_image_fallback_path),
show_icon = true,
timeout = 10,
})
end
UIManager:close(sample_input)
menu:updateItems()
end,
},
}
},
}
UIManager:show(sample_input)
sample_input:onShowKeyboard()
end,
},
-- menu entry: fallback
{
text = _("Turn on fallback image"),
checked_func = function()
return self:_fallback()
end,
callback = function()
self.fallback = not self.fallback
G_reader_settings:saveSetting("cover_image_fallback", self.fallback)
end,
separator = true,
},
},
}
end
return CoverImage