2022-01-04 20:58:56 +00:00
|
|
|
local Blitbuffer = require("ffi/blitbuffer")
|
|
|
|
local Cache = require("cache")
|
|
|
|
local Device = require("device")
|
|
|
|
local Geom = require("ui/geometry")
|
|
|
|
local Persist = require("persist")
|
|
|
|
local RenderImage = require("ui/renderimage")
|
|
|
|
local TileCacheItem = require("document/tilecacheitem")
|
|
|
|
local UIManager = require("ui/uimanager")
|
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
|
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
2022-01-04 20:58:56 +00:00
|
|
|
local Screen = Device.screen
|
|
|
|
local ffiutil = require("ffi/util")
|
|
|
|
local logger = require("logger")
|
|
|
|
local util = require("util")
|
|
|
|
local _ = require("gettext")
|
|
|
|
|
|
|
|
-- This ReaderThumbnail module provides a service for generating thumbnails
|
|
|
|
-- of book pages.
|
|
|
|
-- It handles launching via the menu or Dispatcher/Gestures two fullscreen
|
|
|
|
-- widgets related to showing pages and thumbnails that will make use of
|
|
|
|
-- its services: BookMap and PageBrowser.
|
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
|
|
|
local ReaderThumbnail = WidgetContainer:extend{}
|
2022-01-04 20:58:56 +00:00
|
|
|
|
|
|
|
function ReaderThumbnail:init()
|
|
|
|
if not Device:isTouchDevice() then
|
|
|
|
-- The BookMap and PageBrowser widgets depend too much on gestures,
|
|
|
|
-- making them work with keys would be hard and very limited, so
|
|
|
|
-- just don't make them available.
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
self.ui.menu:registerToMainMenu(self)
|
|
|
|
|
|
|
|
-- Use LuaJIT fast buffer.encode()/decode() when serializing BlitBuffer
|
|
|
|
-- for exchange between subprocess and parent.
|
|
|
|
self.codec = Persist.getCodec("luajit")
|
|
|
|
|
|
|
|
self:setupColor()
|
|
|
|
self.thumbnails_requests = {}
|
|
|
|
self.current_target_size_tag = nil
|
|
|
|
|
|
|
|
-- Ensure no multiple executions, and nextTick() the scheduleIn()
|
|
|
|
-- so we get a chance to process events in-between refreshes and
|
|
|
|
-- this can be interrupted (otherwise, something scheduleIn(0.1),
|
|
|
|
-- if a screen refresh is then done and taking longer than 0.1s,
|
|
|
|
-- would be executed immediately, without emptying any input event).
|
|
|
|
local schedule_step = 0
|
|
|
|
self._ensureTileGeneration_action = function(restart)
|
|
|
|
if restart then
|
|
|
|
UIManager:unschedule(self._ensureTileGeneration_action)
|
|
|
|
schedule_step = 0
|
|
|
|
end
|
|
|
|
if schedule_step == 0 then
|
|
|
|
schedule_step = 1
|
|
|
|
UIManager:nextTick(self._ensureTileGeneration_action)
|
|
|
|
elseif schedule_step == 1 then
|
|
|
|
schedule_step = 2
|
|
|
|
UIManager:scheduleIn(0.1, self._ensureTileGeneration_action)
|
|
|
|
else
|
|
|
|
schedule_step = 0
|
|
|
|
self:ensureTileGeneration()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:addToMainMenu(menu_items)
|
|
|
|
menu_items.book_map = {
|
|
|
|
text = _("Book map"),
|
|
|
|
callback = function()
|
|
|
|
self:onShowBookMap()
|
|
|
|
end,
|
2023-06-02 18:44:47 +00:00
|
|
|
-- Show the alternative overview mode (which is just a restricted
|
|
|
|
-- variation of the main book map) with long-press (let's avoid
|
|
|
|
-- adding another item in the crowded first menu).
|
|
|
|
hold_callback = function()
|
|
|
|
self:onShowBookMap(true)
|
|
|
|
end,
|
2022-01-04 20:58:56 +00:00
|
|
|
}
|
|
|
|
menu_items.page_browser = {
|
|
|
|
text = _("Page browser"),
|
|
|
|
callback = function()
|
|
|
|
self:onShowPageBrowser()
|
|
|
|
end,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2023-06-02 18:44:47 +00:00
|
|
|
function ReaderThumbnail:onShowBookMap(overview_mode)
|
2022-01-04 20:58:56 +00:00
|
|
|
local BookMapWidget = require("ui/widget/bookmapwidget")
|
|
|
|
UIManager:show(BookMapWidget:new{
|
|
|
|
ui = self.ui,
|
2023-06-02 18:44:47 +00:00
|
|
|
overview_mode = overview_mode,
|
2022-01-04 20:58:56 +00:00
|
|
|
})
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:onShowPageBrowser()
|
|
|
|
local PageBrowserWidget = require("ui/widget/pagebrowserwidget")
|
|
|
|
UIManager:show(PageBrowserWidget:new{
|
|
|
|
ui = self.ui,
|
|
|
|
})
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
-- This is made a module local so we can keep track of pids
|
|
|
|
-- to collect across multiple Reader instantiations
|
|
|
|
local pids_to_collect = {}
|
|
|
|
|
|
|
|
function ReaderThumbnail:collectPids()
|
|
|
|
if #pids_to_collect == 0 then
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
for i=#pids_to_collect, 1, -1 do
|
|
|
|
if ffiutil.isSubProcessDone(pids_to_collect[i]) then
|
|
|
|
table.remove(pids_to_collect, i)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return #pids_to_collect > 0
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:setupColor()
|
|
|
|
self.bb_type = self.ui.document.render_color and self.ui.document.color_bb_type or Blitbuffer.TYPE_BB8
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:setupCache()
|
|
|
|
if not self.tile_cache then
|
|
|
|
-- We want to allow browsing at least N pages worth of thumbnails
|
|
|
|
-- without cache trashing. A little more than N pages (because inter
|
|
|
|
-- thumbnail margins) will fit in N * screen size.
|
|
|
|
-- With N=5, this should use from 5 to 15 Mb on a classic eInk device.
|
|
|
|
local N = 5
|
|
|
|
local max_bytes = math.ceil(N * Screen:getWidth() * Screen:getHeight() * Blitbuffer.TYPE_TO_BPP[self.bb_type] / 8)
|
|
|
|
-- We don't really care about limiting any number of slots, so allow
|
|
|
|
-- for at least 5 pages of 10x10 tiles
|
2022-10-10 20:21:27 +00:00
|
|
|
local avg_itemsize = math.ceil(max_bytes * (1/500))
|
2022-01-04 20:58:56 +00:00
|
|
|
self.tile_cache = Cache:new{
|
|
|
|
size = max_bytes,
|
|
|
|
avg_itemsize = avg_itemsize, -- will make slots=500
|
|
|
|
enable_eviction_cb = true,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:logCacheSize()
|
|
|
|
logger.info(string.format("Thumbnails cache: %d/%d (%s/%s)",
|
|
|
|
self.tile_cache.cache.used_slots(),
|
|
|
|
self.tile_cache.slots,
|
|
|
|
util.getFriendlySize(self.tile_cache.cache.used_size()),
|
|
|
|
util.getFriendlySize(self.tile_cache.size)))
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:resetCache()
|
|
|
|
if self.tile_cache then
|
|
|
|
self.tile_cache:clear()
|
|
|
|
self.tile_cache = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:removeFromCache(hash_subs, remove_only_non_matching)
|
|
|
|
-- Remove from cache all tiles matching any hash from hash_subs.
|
|
|
|
-- IF only_non_matching=true, keep those matching and remove all others.
|
|
|
|
if not self.tile_cache then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
if type(hash_subs) ~= "table" then
|
|
|
|
hash_subs = { hash_subs }
|
|
|
|
end
|
|
|
|
local nb_removed, size_removed = 0, 0
|
|
|
|
local to_remove = {}
|
|
|
|
for thash, tile in self.tile_cache.cache:pairs() do
|
|
|
|
local remove = remove_only_non_matching
|
|
|
|
for _, h in ipairs(hash_subs) do
|
|
|
|
if thash:find(h, 1, true) then -- plain text match (no pattern needed)
|
|
|
|
remove = not remove
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if remove then
|
|
|
|
to_remove[thash] = true
|
|
|
|
nb_removed = nb_removed + 1
|
|
|
|
size_removed = size_removed + tile.size
|
|
|
|
end
|
|
|
|
end
|
|
|
|
for thash, _ in pairs(to_remove) do
|
|
|
|
self.tile_cache.cache:delete(thash)
|
|
|
|
logger.dbg("removed cached thumbnail", thash)
|
|
|
|
end
|
|
|
|
return nb_removed, size_removed
|
|
|
|
end
|
|
|
|
|
2024-05-03 06:08:57 +00:00
|
|
|
function ReaderThumbnail:resetCachedPagesForBookmarks(annotations)
|
2022-01-04 20:58:56 +00:00
|
|
|
-- Multiple bookmarks may be provided
|
|
|
|
local start_page, end_page
|
2024-05-03 06:08:57 +00:00
|
|
|
for i = 1, #annotations do
|
|
|
|
local bm = annotations[i]
|
2022-01-04 20:58:56 +00:00
|
|
|
if self.ui.rolling then
|
|
|
|
-- Look at all properties that may be xpointers
|
|
|
|
for _, k in ipairs({"page", "pos0", "pos1"}) do
|
|
|
|
if bm[k] and type(bm[k]) == "string" then
|
|
|
|
local p = self.ui.document:getPageFromXPointer(bm[k])
|
|
|
|
if not start_page or p < start_page then
|
|
|
|
start_page = p
|
|
|
|
end
|
|
|
|
if not end_page or p > end_page then
|
|
|
|
end_page = p
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
if bm.page and type(bm.page) == "number" then
|
2022-12-02 18:22:27 +00:00
|
|
|
local bm_page0 = (bm.pos0 and bm.pos0.page) or bm.page
|
|
|
|
local bm_page1 = (bm.pos1 and bm.pos1.page) or bm.page
|
|
|
|
for p = bm_page0, bm_page1 do
|
|
|
|
if not start_page or p < start_page then
|
|
|
|
start_page = p
|
|
|
|
end
|
|
|
|
if not end_page or p > end_page then
|
|
|
|
end_page = p
|
|
|
|
end
|
2022-01-04 20:58:56 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if start_page and end_page then
|
|
|
|
local hash_subs_to_remove = {}
|
|
|
|
for p=start_page, end_page do
|
|
|
|
table.insert(hash_subs_to_remove, string.format("p%d-", p))
|
|
|
|
end
|
|
|
|
self:removeFromCache(hash_subs_to_remove)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:tidyCache()
|
|
|
|
if self.current_target_size_tag then
|
|
|
|
-- Remove all thumbnails generated for an older target size
|
|
|
|
self:removeFromCache("-"..self.current_target_size_tag, true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:cancelPageThumbnailRequests(batch_id)
|
|
|
|
if batch_id then
|
|
|
|
self.thumbnails_requests[batch_id] = nil
|
|
|
|
else
|
|
|
|
self.thumbnails_requests = {}
|
|
|
|
end
|
|
|
|
if self.req_in_progress and (not batch_id or self.req_in_progress.batch_id == batch_id) then
|
|
|
|
-- Kill any reference to the module cancelling it
|
|
|
|
self.req_in_progress.when_generated_callback = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:getPageThumbnail(page, width, height, batch_id, when_generated_callback)
|
|
|
|
self:setupCache()
|
|
|
|
self.current_target_size_tag = string.format("w%d_h%d", width, height)
|
2023-11-10 19:36:57 +00:00
|
|
|
if self.ui.rolling and Screen.night_mode and self.ui.document.configurable.nightmode_images == 1 then
|
|
|
|
-- We'll get a different bb in this case: it needs its own cache hash
|
|
|
|
self.current_target_size_tag = self.current_target_size_tag .. "_nm"
|
|
|
|
end
|
2022-01-04 20:58:56 +00:00
|
|
|
local hash = string.format("p%d-%s", page, self.current_target_size_tag)
|
|
|
|
local tile = self.tile_cache and self.tile_cache:check(hash)
|
|
|
|
if tile then
|
|
|
|
-- Cached: call callback and we're done.
|
|
|
|
when_generated_callback(tile, batch_id, false)
|
|
|
|
return false -- not delayed
|
|
|
|
end
|
|
|
|
if not self.thumbnails_requests[batch_id] then
|
|
|
|
self.thumbnails_requests[batch_id] = {}
|
|
|
|
end
|
|
|
|
table.insert(self.thumbnails_requests[batch_id], {
|
|
|
|
batch_id = batch_id,
|
|
|
|
hash = hash,
|
|
|
|
page = page,
|
|
|
|
width = width,
|
|
|
|
height = height,
|
|
|
|
when_generated_callback = when_generated_callback,
|
|
|
|
})
|
|
|
|
-- Start tile generation, avoid multiple ones
|
|
|
|
self._ensureTileGeneration_action(true)
|
|
|
|
return true -- delayed
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:ensureTileGeneration()
|
2024-01-15 15:30:12 +00:00
|
|
|
if not self._standby_prevented then
|
|
|
|
self._standby_prevented = true
|
|
|
|
UIManager:preventStandby()
|
|
|
|
end
|
2022-01-04 20:58:56 +00:00
|
|
|
local has_pids_still_to_collect = self:collectPids()
|
|
|
|
|
|
|
|
local still_in_progress = false
|
|
|
|
if self.req_in_progress then
|
|
|
|
local pid_still_to_collect
|
|
|
|
still_in_progress, pid_still_to_collect = self:checkTileGeneration(self.req_in_progress)
|
|
|
|
if pid_still_to_collect then
|
|
|
|
has_pids_still_to_collect = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if not still_in_progress then
|
|
|
|
self.req_in_progress = nil
|
|
|
|
while true do
|
|
|
|
local req_id, requests = next(self.thumbnails_requests)
|
|
|
|
if not req_id then -- no more requests
|
|
|
|
break
|
|
|
|
end
|
|
|
|
local req = table.remove(requests, 1)
|
|
|
|
if #requests == 0 then
|
|
|
|
self.thumbnails_requests[req_id] = nil
|
|
|
|
end
|
|
|
|
if req.when_generated_callback then -- not cancelled since queued
|
|
|
|
-- It might have been generated and cached by a previous batch
|
|
|
|
local tile = self.tile_cache and self.tile_cache:check(req.hash)
|
|
|
|
if tile then
|
|
|
|
req.when_generated_callback(tile, req.batch_id, true)
|
|
|
|
else
|
|
|
|
if self:startTileGeneration(req) then
|
|
|
|
self.req_in_progress = req
|
|
|
|
break
|
|
|
|
else
|
|
|
|
-- Failure starting it: let requester know in case it cares, and forget it
|
|
|
|
req.when_generated_callback(nil, req.batch_id, true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if self.req_in_progress or has_pids_still_to_collect or next(self.thumbnails_requests) then
|
|
|
|
self._ensureTileGeneration_action()
|
2024-01-15 15:30:12 +00:00
|
|
|
else
|
|
|
|
if self._standby_prevented then
|
|
|
|
self._standby_prevented = false
|
|
|
|
UIManager:allowStandby()
|
|
|
|
end
|
2022-01-04 20:58:56 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:startTileGeneration(request)
|
|
|
|
local pid, parent_read_fd = ffiutil.runInSubProcess(function(pid, child_write_fd)
|
|
|
|
-- Get page image as if drawn on the screen
|
|
|
|
local bb = self:_getPageImage(request.page)
|
|
|
|
-- Scale it to fit in the requested size
|
|
|
|
local scale_factor = math.min(request.width / bb:getWidth(), request.height / bb:getHeight())
|
|
|
|
local target_w = math.floor(bb:getWidth() * scale_factor)
|
|
|
|
local target_h = math.floor(bb:getHeight() * scale_factor)
|
2022-05-05 19:00:22 +00:00
|
|
|
-- local time = require("ui/time")
|
|
|
|
-- local start_time = time.now()
|
2022-01-04 20:58:56 +00:00
|
|
|
local tile = TileCacheItem:new{
|
|
|
|
bb = RenderImage:scaleBlitBuffer(bb, target_w, target_h, true),
|
|
|
|
pageno = request.page,
|
|
|
|
}
|
|
|
|
tile.size = tonumber(tile.bb.stride) * tile.bb.h
|
|
|
|
-- logger.info("tile size", tile.bb.w, tile.bb.h, "=>", tile.size)
|
2022-05-05 19:00:22 +00:00
|
|
|
-- logger.info(string.format(" scaling took %.3f seconds, %d bpp", time.to_s(time.since(start_time)), tile.bb:getBpp()))
|
2022-01-04 20:58:56 +00:00
|
|
|
-- bb:free() -- no need to spend time freeing, we're dying soon anyway!
|
|
|
|
|
|
|
|
ffiutil.writeToFD(child_write_fd, self.codec.serialize(tile:totable()), true)
|
|
|
|
end, true) -- with_pipe = true
|
|
|
|
if pid then
|
|
|
|
-- Store these in the request object itself
|
|
|
|
request.pid = pid
|
|
|
|
request.parent_read_fd = parent_read_fd
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
logger.warn("PageBrowserWidget thumbnail start failure:", parent_read_fd)
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:checkTileGeneration(request)
|
|
|
|
local pid, parent_read_fd = request.pid, request.parent_read_fd
|
|
|
|
local stuff_to_read = ffiutil.getNonBlockingReadSize(parent_read_fd) ~= 0
|
|
|
|
local subprocess_done = ffiutil.isSubProcessDone(pid)
|
|
|
|
logger.dbg("subprocess_done:", subprocess_done, " stuff_to_read:", stuff_to_read)
|
|
|
|
if stuff_to_read then
|
2022-05-05 19:00:22 +00:00
|
|
|
-- local time = require("ui/time")
|
|
|
|
-- local start_time = time.now()
|
2022-01-04 20:58:56 +00:00
|
|
|
local result, err = self.codec.deserialize(ffiutil.readAllFromFD(parent_read_fd))
|
|
|
|
if result then
|
|
|
|
local tile = TileCacheItem:new{}
|
|
|
|
tile:fromtable(result)
|
|
|
|
if self.tile_cache then
|
|
|
|
self.tile_cache:insert(request.hash, tile)
|
|
|
|
end
|
|
|
|
if request.when_generated_callback then -- not cancelled
|
|
|
|
request.when_generated_callback(tile, request.batch_id, true)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
logger.warn("PageBrowserWidget thumbnail deserialize() failed:", err)
|
|
|
|
if request.when_generated_callback then -- not cancelled
|
|
|
|
request.when_generated_callback(nil, request.batch_id, true)
|
|
|
|
end
|
|
|
|
end
|
2022-05-05 19:00:22 +00:00
|
|
|
-- logger.info(string.format(" parsing result from subprocess took %.3f seconds", time.to_s(time.since(start_time))))
|
2022-01-04 20:58:56 +00:00
|
|
|
if not subprocess_done then
|
|
|
|
table.insert(pids_to_collect, pid)
|
|
|
|
return false, true
|
|
|
|
end
|
|
|
|
return false
|
|
|
|
elseif subprocess_done then
|
|
|
|
-- subprocess_done: process exited with no output
|
|
|
|
ffiutil.readAllFromFD(parent_read_fd) -- close our fd
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
logger.dbg("process not yet done, will check again soon")
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:_getPageImage(page)
|
|
|
|
-- This is run in a subprocess: we can tweak all document settings
|
|
|
|
-- to get an adequate image of the page.
|
|
|
|
-- No need to worry about the final state of things: this subprocess
|
|
|
|
-- will die just after drawing the page, and all will be forgotten,
|
|
|
|
-- without impact on the parent process.
|
|
|
|
|
|
|
|
-- Be sure to limit our impact on the disk-saved book state
|
|
|
|
self.ui.saveSettings = function() end -- Be sure nothing is flushed
|
|
|
|
self.ui.statistics = nil -- Don't update statistics for pages we visit
|
|
|
|
|
|
|
|
-- By default, our target page size is the current screen size
|
|
|
|
local target_w, target_h = Screen:getWidth(), Screen:getHeight()
|
|
|
|
|
|
|
|
-- This was all mostly chosen by experimenting.
|
|
|
|
-- Be sure to call the innermost methods enough to get what we want, and
|
|
|
|
-- not upper event handlers that may trigger other unneeded events and stuff.
|
|
|
|
-- Especially, be sure to not trigger any paint on the screen buffer, or
|
|
|
|
-- any processing of input events.
|
|
|
|
-- No need to worry about UIManager:scheduleIn() or :nextTick(), as
|
|
|
|
-- we will die before the callback gets a chance to be run.
|
|
|
|
|
|
|
|
-- Common to ReaderRolling and ReaderPaging
|
|
|
|
self.ui.view.footer_visible = false -- We want no footer on page image
|
|
|
|
if self.ui.view.highlight.lighten_factor < 0.3 then
|
|
|
|
self.ui.view.highlight.lighten_factor = 0.3 -- make lighten highlight a bit darker
|
|
|
|
end
|
ReaderRolling: quicker partial rerenderings with EPUBs
Only available with EPUBs containing 2 or more fragments,
and a file size large enough to ensure a cache file is used.
The idea is simply, on any rendering setting change, to
skip the rerendering of the full book and to defer any
rerendering to the moment we draw a DocFragment, and
render only it.
So, on a setting change, only the fragment containing the
current page will be rerendered, and the new fragments we
may cross while turning pages.
When having done so, KOReader is in a degraded state (the
full page count is incorrect, the ToC is invalid...).
So, a full rerendering is needed, and one will happen
in the background, and when the user is idle, we reload
seamlessly and quickly from the cache file it has made.
ReaderFlipping will show some icons in the top left
corner to let it know at which steps in this procress
we are.
2023-02-16 22:30:58 +00:00
|
|
|
self.ui.highlight.select_mode = false -- Remove any select mode icon
|
2022-01-04 20:58:56 +00:00
|
|
|
|
|
|
|
if self.ui.rolling then
|
|
|
|
-- CRE documents: pages all have the aspect ratio of our screen (alt top status bar
|
|
|
|
-- will be croped out after drawing), we will show them just as rendered.
|
ReaderRolling: quicker partial rerenderings with EPUBs
Only available with EPUBs containing 2 or more fragments,
and a file size large enough to ensure a cache file is used.
The idea is simply, on any rendering setting change, to
skip the rerendering of the full book and to defer any
rerendering to the moment we draw a DocFragment, and
render only it.
So, on a setting change, only the fragment containing the
current page will be rerendered, and the new fragments we
may cross while turning pages.
When having done so, KOReader is in a degraded state (the
full page count is incorrect, the ToC is invalid...).
So, a full rerendering is needed, and one will happen
in the background, and when the user is idle, we reload
seamlessly and quickly from the cache file it has made.
ReaderFlipping will show some icons in the top left
corner to let it know at which steps in this procress
we are.
2023-02-16 22:30:58 +00:00
|
|
|
self.ui.rolling.rendering_state = nil -- Remove any partial rerendering icon
|
2023-10-08 15:51:41 +00:00
|
|
|
if self.ui.view.view_mode == "scroll" then
|
|
|
|
-- Get out of scroll mode, and be sure we'll be in one-page mode as that
|
|
|
|
-- is what is shown in scroll mode (needs to do the following in that
|
|
|
|
-- order to avoid rendering hash change)
|
|
|
|
self.ui.rolling:onSetVisiblePages(1)
|
|
|
|
self.ui.view:onSetViewMode("page")
|
|
|
|
end
|
2023-09-09 09:44:56 +00:00
|
|
|
if self.ui.document.configurable.font_gamma < 30 then -- Increase font gamma (if not already increased),
|
2022-01-04 20:58:56 +00:00
|
|
|
self.ui.document:setGammaIndex(30) -- as downscaling will make text grayer
|
|
|
|
end
|
|
|
|
self.ui.document:setImageScaling(false) -- No need for smooth scaling as all will be downscaled
|
2023-11-10 19:36:57 +00:00
|
|
|
-- (We keep "nighmode_images" as it was set: we may get and cache a different bb whether nightmode is on or off)
|
2022-01-04 20:58:56 +00:00
|
|
|
self.ui.view.state.page = page -- Be on requested page
|
|
|
|
self.ui.document:gotoPage(page) -- Current xpointer needs to be updated for some of what follows
|
|
|
|
self.ui.bookmark:onPageUpdate(page) -- Update dogear state for this page
|
|
|
|
self.ui.pagemap:onPageUpdate(page) -- Update pagemap labels for this page
|
|
|
|
end
|
|
|
|
|
|
|
|
if self.ui.paging then
|
|
|
|
-- With PDF/DJVU/Pics, we will show the native page (no reflow, no crop, no zoom
|
|
|
|
-- to columns...). This makes thumbnail generation faster, and will allow the user
|
|
|
|
-- to get an overview of the book native pages to better decide which option will
|
|
|
|
-- be best to use for the book.
|
|
|
|
-- We also want to get a thumbnail with the aspect ratio of the native page
|
|
|
|
-- (so we don't get a native landscape page smallish and centered with blank above
|
|
|
|
-- and below in a portrait thumbnail, if the screen is in portrait mode).
|
|
|
|
|
|
|
|
self.ui.view.hinting = false -- Disable hinting
|
|
|
|
self.ui.view.page_scroll = false -- Get out of scroll mode
|
|
|
|
self.ui.view.flipping_visible = false -- No page flipping icon
|
|
|
|
self.ui.document.configurable.text_wrap = false -- Get out of reflow mode
|
|
|
|
self.ui.document.configurable.trim_page = 3 -- Page crop: none
|
|
|
|
-- self.ui.document.configurable.trim_page = 1 -- Page crop: auto (very slower)
|
|
|
|
self.ui.document.configurable.auto_straighten = 0 -- No auto straighten
|
|
|
|
-- We can let dewatermark if the user has enabled it, it helps
|
|
|
|
-- limiting annoying eInk refreshes of light gray areas
|
|
|
|
-- self.ui.document.configurable.page_opt = 0 -- No dewatermark
|
|
|
|
-- We won't touch the contrast (to try making text less gray), as it applies on
|
|
|
|
-- images that could get too dark.
|
|
|
|
|
|
|
|
-- Get native page dimensions, and update our target bb dimensions so it gets the
|
|
|
|
-- same aspect ratio (we don't use native dimensions as is, as they may get huge)
|
|
|
|
local dimen = self.ui.document:getPageDimensions(page, 1, 0)
|
|
|
|
local scale_factor = math.min(target_w / dimen.w, target_h / dimen.h)
|
|
|
|
target_w = math.floor(dimen.w * scale_factor)
|
|
|
|
target_h = math.floor(dimen.h * scale_factor)
|
|
|
|
dimen = Geom:new{ w=target_w, h=target_h }
|
|
|
|
-- logger.info("getPageImage", page, dimen, "=>", target_w, target_h, scale_factor)
|
|
|
|
|
|
|
|
-- This seems to do it all well:
|
|
|
|
-- local Event = require("ui/event")
|
|
|
|
-- self.ui:handleEvent(Event:new("SetDimensions", dimen))
|
|
|
|
-- self.ui.view.dogear[1].dimen.w = dimen.w -- (hack... its code uses the Screen width)
|
|
|
|
-- self.ui:handleEvent(Event:new("PageUpdate", page))
|
|
|
|
-- self.ui:handleEvent(Event:new("SetZoomMode", "page"))
|
|
|
|
|
|
|
|
-- Trying to do as little as needed, knowing the internals:
|
|
|
|
self.ui.view:onSetDimensions(dimen)
|
|
|
|
self.ui.view:onBBoxUpdate(nil) -- drop any bbox, draw native page
|
|
|
|
self.ui.view.state.page = page
|
|
|
|
self.ui.view.state.zoom = scale_factor
|
|
|
|
self.ui.view.state.rotation = 0
|
|
|
|
self.ui.view:recalculate()
|
|
|
|
self.ui.view.dogear[1].dimen.w = dimen.w -- (hack... its code uses the Screen width)
|
|
|
|
self.ui.bookmark:onPageUpdate(page) -- Update dogear state for this page
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Draw the page on a new BB with the targetted size
|
|
|
|
local bb = Blitbuffer.new(target_w, target_h, self.bb_type)
|
|
|
|
self.ui.view:paintTo(bb, 0, 0)
|
|
|
|
|
|
|
|
if self.ui.rolling then
|
|
|
|
-- Crop out the top alt status bar if enabled
|
|
|
|
local header_height = self.ui.document:getHeaderHeight()
|
|
|
|
if header_height > 0 then
|
|
|
|
bb = bb:viewport(0, header_height, bb.w, bb.h - header_height)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return bb
|
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:onCloseDocument()
|
|
|
|
self:cancelPageThumbnailRequests()
|
|
|
|
if self.tile_cache then
|
|
|
|
self:logCacheSize()
|
|
|
|
self.tile_cache:clear()
|
|
|
|
self.tile_cache = nil
|
|
|
|
end
|
2024-01-15 15:30:12 +00:00
|
|
|
if self._standby_prevented then
|
|
|
|
self._standby_prevented = false
|
|
|
|
UIManager:allowStandby()
|
|
|
|
end
|
2022-01-04 20:58:56 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function ReaderThumbnail:onColorRenderingUpdate()
|
|
|
|
self:setupColor()
|
|
|
|
self:resetCache()
|
|
|
|
end
|
|
|
|
|
|
|
|
-- CRE: emitted after a re-rendering
|
2022-10-25 11:16:01 +00:00
|
|
|
ReaderThumbnail.onDocumentRerendered = ReaderThumbnail.resetCache
|
ReaderRolling: quicker partial rerenderings with EPUBs
Only available with EPUBs containing 2 or more fragments,
and a file size large enough to ensure a cache file is used.
The idea is simply, on any rendering setting change, to
skip the rerendering of the full book and to defer any
rerendering to the moment we draw a DocFragment, and
render only it.
So, on a setting change, only the fragment containing the
current page will be rerendered, and the new fragments we
may cross while turning pages.
When having done so, KOReader is in a degraded state (the
full page count is incorrect, the ToC is invalid...).
So, a full rerendering is needed, and one will happen
in the background, and when the user is idle, we reload
seamlessly and quickly from the cache file it has made.
ReaderFlipping will show some icons in the top left
corner to let it know at which steps in this procress
we are.
2023-02-16 22:30:58 +00:00
|
|
|
ReaderThumbnail.onDocumentPartiallyRerendered = ReaderThumbnail.resetCache
|
2022-01-04 20:58:56 +00:00
|
|
|
-- Emitted When adding/removing/updating bookmarks and highlights
|
2024-05-03 06:08:57 +00:00
|
|
|
ReaderThumbnail.onAnnotationsModified = ReaderThumbnail.resetCachedPagesForBookmarks
|
2022-01-04 20:58:56 +00:00
|
|
|
|
|
|
|
return ReaderThumbnail
|