Merge pull request #6976 from NiLuJe/cbb-c99-front

* BookInfoManager:
  * Use ZSTD instead of zlib to compress thumbnails, because zlib is the absolute worst in terms of performance nowadays.
     Like in CRe, slightly smaller DB, slightly faster compression, hilariously faster decompression.
  * Also revamps the schema a tiny bit following recent discussions:
    * Added `filesize`, `filemtime` columns, and split `series` into `series` and `series_index`.
  * Made the DB migration slightly less harsh (i.e., preserve settings, and a bit of visual feedbad).

* A few `__gc` metamethod tweaks as mentioned in base.
reviewable/pr6977/r1
NiLuJe 3 years ago committed by GitHub
commit 84ac1cae05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -11,11 +11,8 @@ echo "wrap_bin_scripts = false" >>"${HOME}/.luarocks/config.lua"
travis_retry luarocks --local install luafilesystem travis_retry luarocks --local install luafilesystem
# for verbose_print module # for verbose_print module
travis_retry luarocks --local install ansicolors travis_retry luarocks --local install ansicolors
travis_retry luarocks --local install busted 2.0.rc13-0 travis_retry luarocks --local install busted 2.0.0-1
#- mv -f $HOME/.luarocks/bin/busted_bootstrap $HOME/.luarocks/bin/busted #- mv -f $HOME/.luarocks/bin/busted_bootstrap $HOME/.luarocks/bin/busted
# Apply junit testcase time fix. This can be removed once there is a busted 2.0.rc13 or final
# See https://github.com/Olivine-Labs/busted/commit/830f175c57ca3f9e79f95b8c4eaacf58252453d7
sed -i 's|testcase_node.time = formatDuration(element.duration)|testcase_node:set_attrib("time", formatDuration(element.duration))|' "${HOME}/.luarocks/share/lua/5.1/busted/outputHandlers/junit.lua"
travis_retry luarocks --local install luacheck travis_retry luarocks --local install luacheck
travis_retry luarocks --local install lanes # for parallel luacheck travis_retry luarocks --local install lanes # for parallel luacheck

@ -1 +1 @@
Subproject commit 6924f35412469bf8f82c047e05bd0f9e9ceefa96 Subproject commit b00f7f1370ec48cd0fb20f662d9974f817aeb6a4

@ -157,8 +157,15 @@ function BookInfo:show(file, book_props)
local series = book_props.series local series = book_props.series
if series == "" or series == nil then if series == "" or series == nil then
series = _("N/A") series = _("N/A")
else -- Shorten calibre series decimal number (#4.0 => #4) else
series = series:gsub("(#%d+)%.0$", "%1") -- If we were fed a BookInfo book_props (e.g., covermenu), series index is in a separate field
if book_props.series_index then
-- Here, we're assured that series_index is a Lua number, so round integers are automatically displayed without decimals
series = book_props.series .. " #" .. book_props.series_index
else
-- But here, if we have a plain doc_props series with an index, drop empty decimals from round integers.
series = book_props.series:gsub("(#%d+)%.0+$", "%1")
end
end end
table.insert(kv_pairs, { _("Series:"), BD.auto(series) }) table.insert(kv_pairs, { _("Series:"), BD.auto(series) })

@ -125,13 +125,6 @@ function KindlePowerD:isChargingHW()
return is_charging == 1 return is_charging == 1
end end
function KindlePowerD:__gc()
if self.lipc_handle then
self.lipc_handle:close()
self.lipc_handle = nil
end
end
function KindlePowerD:_readFLIntensity() function KindlePowerD:_readFLIntensity()
return self:read_int_file(self.fl_intensity_file) return self:read_int_file(self.fl_intensity_file)
end end
@ -161,4 +154,12 @@ function KindlePowerD:toggleSuspend()
end end
end end
--- @fixme: This won't ever fire, as KindlePowerD is already a metatable on a plain table.
function KindlePowerD:__gc()
if self.lipc_handle then
self.lipc_handle:close()
self.lipc_handle = nil
end
end
return KindlePowerD return KindlePowerD

@ -15,7 +15,7 @@ end
function TileCacheItem:dump(filename) function TileCacheItem:dump(filename)
logger.dbg("dumping tile cache to", filename, self.excerpt) logger.dbg("dumping tile cache to", filename, self.excerpt)
return serial.dump(self.size, self.excerpt, self.pageno, return serial.dump(self.size, self.excerpt, self.pageno,
self.bb.w, self.bb.h, self.bb.stride, self.bb:getType(), self.bb.w, self.bb.h, tonumber(self.bb.stride), self.bb:getType(),
Blitbuffer.tostring(self.bb), filename) Blitbuffer.tostring(self.bb), filename)
end end

@ -102,7 +102,7 @@ function RenderImage:renderGifImageDataWithGifLib(data, size, want_frames, width
if want_frames and nb_frames > 1 then if want_frames and nb_frames > 1 then
-- Returns a regular table, with functions (returning the BlitBuffer) -- Returns a regular table, with functions (returning the BlitBuffer)
-- as values. Users will have to check via type() and call them. -- as values. Users will have to check via type() and call them.
-- (our luajit does not support __len via metatable, otherwise we -- (The __len metamethod is a Lua 5.2 feature, otherwise we
-- could have used setmetatable to avoid creating all the functions) -- could have used setmetatable to avoid creating all the functions)
local frames = {} local frames = {}
-- As we don't cache the bb we build on the fly, let caller know it -- As we don't cache the bb we build on the fly, let caller know it
@ -118,27 +118,30 @@ function RenderImage:renderGifImageDataWithGifLib(data, size, want_frames, width
end) end)
end end
-- We can't close our GifDocument as long as we may fetch some -- We can't close our GifDocument as long as we may fetch some
-- frame: we need to delay it till 'frames' is no more used. -- frame: we need to delay it till 'frames' is no longer used.
frames.gif_close_needed = true frames.gif_close_needed = true
-- Should happen with that, but __gc seems never called... -- Since frames is a plain table, __gc won't work on Lua 5.1/LuaJIT,
frames = setmetatable(frames, { -- not without a little help from the newproxy hack...
__gc = function() frames.gif = gif
logger.dbg("frames.gc() called, closing GifDocument") local frames_mt = {}
if frames.gif_close_needed then function frames_mt:__gc()
gif:close() logger.dbg("frames.gc() called, closing GifDocument", self.gif)
frames.gif_close_needed = nil if self.gif_close_needed then
end self.gif:close()
self.gif_close_needed = nil
end end
}) end
-- so, also set this method, so that ImageViewer can explicitely -- Much like our other stuff, when we're puzzled about __gc, we do it manually!
-- call it onClose. -- So, also set this method, so that ImageViewer can explicitely call it onClose.
frames.free = function() function frames:free()
logger.dbg("frames.free() called, closing GifDocument") logger.dbg("frames.free() called, closing GifDocument", self.gif)
if frames.gif_close_needed then if self.gif_close_needed then
gif:close() self.gif:close()
frames.gif_close_needed = nil self.gif_close_needed = nil
end end
end end
local setmetatable = require("ffi/__gc")
setmetatable(frames, frames_mt)
return frames return frames
else else
local page = gif:openPage(1) local page = gif:openPage(1)

@ -719,7 +719,7 @@ function ImageViewer:onCloseWidget()
end end
-- also clean _images_list if it provides a method for that -- also clean _images_list if it provides a method for that
if self._images_list and self._images_list_disposable and self._images_list.free then if self._images_list and self._images_list_disposable and self._images_list.free then
self._images_list.free() self._images_list:free()
end end
-- NOTE: Assume there's no image beneath us, so, no dithering request -- NOTE: Assume there's no image beneath us, so, no dithering request
UIManager:setDirty(nil, function() UIManager:setDirty(nil, function()

@ -205,7 +205,7 @@ function ImageWidget:_loadfile()
-- cache this image -- cache this image
logger.dbg("cache", hash) logger.dbg("cache", hash)
cache = ImageCacheItem:new{ bb = self._bb } cache = ImageCacheItem:new{ bb = self._bb }
cache.size = cache.bb.stride * cache.bb.h cache.size = tonumber(cache.bb.stride) * cache.bb.h
ImageCache:insert(hash, cache) ImageCache:insert(hash, cache)
end end
end end

@ -810,11 +810,11 @@ end
--- If the given path has a trailing /, returns the entire path as the directory --- If the given path has a trailing /, returns the entire path as the directory
--- path and "" as the file name. --- path and "" as the file name.
---- @string file ---- @string file
---- @treturn string path, filename ---- @treturn string directory, filename
function util.splitFilePathName(file) function util.splitFilePathName(file)
if file == nil or file == "" then return "", "" end if file == nil or file == "" then return "", "" end
if string.find(file, "/") == nil then return "", file end if string.find(file, "/") == nil then return "", file end
return string.gsub(file, "(.*/)(.*)", "%1"), string.gsub(file, ".*/", "") return file:match("(.*/)(.*)")
end end
--- Splits a file name into its pure file name and suffix --- Splits a file name into its pure file name and suffix
@ -823,7 +823,7 @@ end
function util.splitFileNameSuffix(file) function util.splitFileNameSuffix(file)
if file == nil or file == "" then return "", "" end if file == nil or file == "" then return "", "" end
if string.find(file, "%.") == nil then return file, "" end if string.find(file, "%.") == nil then return file, "" end
return string.gsub(file, "(.*)%.(.*)", "%1"), string.gsub(file, ".*%.", "") return file:match("(.*)%.(.*)")
end end
--- Gets file extension --- Gets file extension
@ -835,7 +835,7 @@ function util.getFileNameSuffix(file)
end end
--- Companion helper function that returns the script's language, --- Companion helper function that returns the script's language,
--- based on the filme extension. --- based on the file extension.
---- @string filename ---- @string filename
---- @treturn string (lowercase) (or nil if not Device:canExecuteScript(file)) ---- @treturn string (lowercase) (or nil if not Device:canExecuteScript(file))
function util.getScriptType(file) function util.getScriptType(file)

@ -4,36 +4,37 @@ local DataStorage = require("datastorage")
local Device = require("device") local Device = require("device")
local DocumentRegistry = require("document/documentregistry") local DocumentRegistry = require("document/documentregistry")
local FFIUtil = require("ffi/util") local FFIUtil = require("ffi/util")
local InfoMessage = require("ui/widget/infomessage")
local RenderImage = require("ui/renderimage") local RenderImage = require("ui/renderimage")
local SQ3 = require("lua-ljsqlite3/init") local SQ3 = require("lua-ljsqlite3/init")
local UIManager = require("ui/uimanager") local UIManager = require("ui/uimanager")
local lfs = require("libs/libkoreader-lfs") local lfs = require("libs/libkoreader-lfs")
local logger = require("logger") local logger = require("logger")
local util = require("util") local util = require("util")
local zstd = require("ffi/zstd")
local _ = require("gettext") local _ = require("gettext")
local N_ = _.ngettext local N_ = _.ngettext
local T = FFIUtil.template local T = FFIUtil.template
-- Util functions needed by this plugin, but that may be added to existing base/ffi/ files
local xutil = require("xutil")
-- Database definition -- Database definition
local BOOKINFO_DB_VERSION = "2-20170701" local BOOKINFO_DB_VERSION = 20201210
local BOOKINFO_DB_SCHEMA = [[ local BOOKINFO_DB_SCHEMA = [[
-- For caching book cover and metadata -- To cache book cover and metadata
CREATE TABLE IF NOT EXISTS bookinfo ( CREATE TABLE IF NOT EXISTS bookinfo (
-- Internal book cache id -- Internal book cache id
-- (not to be used to identify a book, it may change for a same book) -- (not to be used to identify a book, it may change)
bcid INTEGER PRIMARY KEY AUTOINCREMENT, bcid INTEGER PRIMARY KEY AUTOINCREMENT,
-- File location and filename -- File location and filename
directory TEXT NOT NULL, -- split by dir/name so we can get all files in a directory directory TEXT NOT NULL, -- split by dir/name so we can get all files in a directory
filename TEXT NOT NULL, -- and can implement pruning of no more existing files filename TEXT NOT NULL, -- and can implement pruning of deleted files
filesize INTEGER, -- size in bytes at most recent extraction time
filemtime INTEGER, -- mtime at most recent extraction time
-- Extraction status and result -- Extraction status and result
in_progress INTEGER, -- 0 (done), >0 : nb of tries (to avoid re-doing extractions that crashed us) in_progress INTEGER, -- 0 (done), >0 : nb of tries (to avoid retrying failed extractions forever)
unsupported TEXT, -- NULL if supported / reason for being unsupported unsupported TEXT, -- NULL if supported / reason for being unsupported
cover_fetched TEXT, -- NULL / 'Y' = action of fetching cover was made (whether we got one or not) cover_fetched TEXT, -- NULL / 'Y' = we tried to fetch a cover (but we may not have gotten one)
has_meta TEXT, -- NULL / 'Y' = has metadata (title, authors...) has_meta TEXT, -- NULL / 'Y' = has metadata (title, authors...)
has_cover TEXT, -- NULL / 'Y' = has cover image (cover_*) has_cover TEXT, -- NULL / 'Y' = has cover image (cover_*)
cover_sizetag TEXT, -- 'M' (Medium, MosaicMenuItem) / 's' (small, ListMenuItem) cover_sizetag TEXT, -- 'M' (Medium, MosaicMenuItem) / 's' (small, ListMenuItem)
@ -50,6 +51,7 @@ local BOOKINFO_DB_SCHEMA = [[
title TEXT, title TEXT,
authors TEXT, authors TEXT,
series TEXT, series TEXT,
series_index REAL,
language TEXT, language TEXT,
keywords TEXT, keywords TEXT,
description TEXT, description TEXT,
@ -57,26 +59,24 @@ local BOOKINFO_DB_SCHEMA = [[
-- Cover image -- Cover image
cover_w INTEGER, -- blitbuffer width cover_w INTEGER, -- blitbuffer width
cover_h INTEGER, -- blitbuffer height cover_h INTEGER, -- blitbuffer height
cover_btype INTEGER, -- blitbuffer type (internal) cover_bb_type INTEGER, -- blitbuffer type (internal)
cover_bpitch INTEGER, -- blitbuffer pitch (internal) cover_bb_stride INTEGER, -- blitbuffer stride (internal)
cover_datalen INTEGER, -- blitbuffer uncompressed data length cover_bb_data BLOB -- blitbuffer data compressed with zstd
cover_dataz BLOB -- blitbuffer data compressed with zlib
); );
CREATE UNIQUE INDEX IF NOT EXISTS dir_filename ON bookinfo(directory, filename); CREATE UNIQUE INDEX IF NOT EXISTS dir_filename ON bookinfo(directory, filename);
-- For keeping track of DB schema version -- To keep track of CoverBrowser settings
CREATE TABLE IF NOT EXISTS config ( CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT value TEXT
); );
-- this will not override previous version value, so we'll get the old one if old schema
INSERT OR IGNORE INTO config VALUES ('version', ']] .. BOOKINFO_DB_VERSION .. [[');
]] ]]
local BOOKINFO_COLS_SET = { local BOOKINFO_COLS_SET = {
"directory", "directory",
"filename", "filename",
"filesize",
"filemtime",
"in_progress", "in_progress",
"unsupported", "unsupported",
"cover_fetched", "cover_fetched",
@ -89,15 +89,15 @@ local BOOKINFO_COLS_SET = {
"title", "title",
"authors", "authors",
"series", "series",
"series_index",
"language", "language",
"keywords", "keywords",
"description", "description",
"cover_w", "cover_w",
"cover_h", "cover_h",
"cover_btype", "cover_bb_type",
"cover_bpitch", "cover_bb_stride",
"cover_datalen", "cover_bb_data",
"cover_dataz",
} }
local bookinfo_values_sql = {} -- for "VALUES (?, ?, ?,...)" insert sql part local bookinfo_values_sql = {} -- for "VALUES (?, ?, ?,...)" insert sql part
@ -150,16 +150,35 @@ function BookInfoManager:createDB()
-- Less error cases to check if we do it that way -- Less error cases to check if we do it that way
-- Create it (noop if already there) -- Create it (noop if already there)
db_conn:exec(BOOKINFO_DB_SCHEMA) db_conn:exec(BOOKINFO_DB_SCHEMA)
-- Check version (not updated by previous exec if already there) -- Check version
local res = db_conn:exec("SELECT value FROM config where key='version';") local db_version = tonumber(db_conn:rowexec("PRAGMA user_version;"))
if res[1][1] ~= BOOKINFO_DB_VERSION then if db_version < BOOKINFO_DB_VERSION then
logger.warn("BookInfo cache DB schema updated from version", res[1][1], "to version", BOOKINFO_DB_VERSION) logger.warn("BookInfo cache DB schema updated from version", db_version, "to version", BOOKINFO_DB_VERSION)
logger.warn("Deleting existing", self.db_location, "to recreate it") logger.warn("Deleting existing", self.db_location, "to recreate it")
-- We'll try to preserve settings, though
self:loadSettings(db_conn)
db_conn:close() db_conn:close()
os.remove(self.db_location) os.remove(self.db_location)
-- Re-create it -- Re-create it
db_conn = SQ3.open(self.db_location) db_conn = SQ3.open(self.db_location)
db_conn:exec(BOOKINFO_DB_SCHEMA) db_conn:exec(BOOKINFO_DB_SCHEMA)
-- Restore non-deprecated settings
for k, v in pairs(self.settings) do
if k ~= "version" then
self:saveSetting(k, v, true)
end
end
self:loadSettings(db_conn)
-- Update version
db_conn:exec(string.format("PRAGMA user_version=%d;", BOOKINFO_DB_VERSION))
-- Say hi!
UIManager:show(InfoMessage:new{text =_("BookInfo cache database schema updated."), timeout = 3 })
end end
db_conn:close() db_conn:close()
self.db_created = true self.db_created = true
@ -216,19 +235,29 @@ function BookInfoManager:compactDb()
end end
-- Settings management, stored in 'config' table -- Settings management, stored in 'config' table
function BookInfoManager:loadSettings() function BookInfoManager:loadSettings(db_conn)
if lfs.attributes(self.db_location, "mode") ~= "file" then if lfs.attributes(self.db_location, "mode") ~= "file" then
-- no db, empty config -- no db, empty config
self.settings = {} self.settings = {}
return return
end end
self.settings = {} self.settings = {}
self:openDbConnection()
local res = self.db_conn:exec("SELECT key, value FROM config;") local my_db_conn
local keys = res[1] if db_conn then
local values = res[2] my_db_conn = db_conn
for i, key in ipairs(keys) do else
self.settings[key] = values[i] self:openDbConnection()
my_db_conn = self.db_conn
end
local res = my_db_conn:exec("SELECT key, value FROM config;")
if res then
local keys = res[1]
local values = res[2]
for i, key in ipairs(keys) do
self.settings[key] = values[i]
end
end end
end end
@ -239,7 +268,7 @@ function BookInfoManager:getSetting(key)
return self.settings[key] return self.settings[key]
end end
function BookInfoManager:saveSetting(key, value) function BookInfoManager:saveSetting(key, value, skip_reload)
if not value or value == false or value == "" then if not value or value == false or value == "" then
if lfs.attributes(self.db_location, "mode") ~= "file" then if lfs.attributes(self.db_location, "mode") ~= "file" then
-- If no db created, no need to save (and create db) an empty value -- If no db created, no need to save (and create db) an empty value
@ -257,8 +286,11 @@ function BookInfoManager:saveSetting(key, value)
stmt:bind(key, value) stmt:bind(key, value)
stmt:step() -- commited stmt:step() -- commited
stmt:clearbind():reset() -- cleanup stmt:clearbind():reset() -- cleanup
-- Reload settings, so we may get (or not if it failed) what we just saved
self:loadSettings() -- Optionally, reload settings, so we may get (or not if it failed) what we just saved
if not skip_reload then
self:loadSettings()
end
end end
-- Bookinfo management -- Bookinfo management
@ -274,6 +306,10 @@ function BookInfoManager:getBookInfo(filepath, get_cover)
return { return {
directory = directory, directory = directory,
filename = filename, filename = filename,
--[[
filesize = lfs.attributes(filepath, "size"),
filemtime = lfs.attributes(filepath, "modification"),
--]]
in_progress = 0, in_progress = 0,
cover_fetched = "Y", cover_fetched = "Y",
has_meta = nil, has_meta = nil,
@ -289,9 +325,11 @@ function BookInfoManager:getBookInfo(filepath, get_cover)
self:openDbConnection() self:openDbConnection()
local row = self.get_stmt:bind(directory, filename):step() local row = self.get_stmt:bind(directory, filename):step()
self.get_stmt:clearbind():reset() -- get ready for next query -- NOTE: We do not reset right now because we'll be querying a BLOB,
-- so we need the data it points to to still be there ;).
if not row then -- filepath not in db if not row then -- filepath not in db
self.get_stmt:clearbind():reset() -- get ready for next query
return nil return nil
end end
@ -313,16 +351,27 @@ function BookInfoManager:getBookInfo(filepath, get_cover)
if bookinfo["has_cover"] then if bookinfo["has_cover"] then
bookinfo["cover_w"] = tonumber(row[num]) bookinfo["cover_w"] = tonumber(row[num])
bookinfo["cover_h"] = tonumber(row[num+1]) bookinfo["cover_h"] = tonumber(row[num+1])
local cover_data = xutil.zlib_uncompress(row[num+5], row[num+4]) local bbtype = tonumber(row[num+2])
row[num+5] = nil -- release memory used by cover_dataz local bbstride = tonumber(row[num+3])
-- Blitbuffer.fromstring() expects : w, h, bb_type, bb_data, pitch -- This is a blob_mt table! Essentially, a (ptr, size) tuple.
bookinfo["cover_bb"] = Blitbuffer.fromstring(row[num], row[num+1], row[num+2], cover_data, row[num+3]) local cover_blob = row[num+4]
-- release memory used by uncompressed data: -- The pointer returned by SQLite is only valid until the next step/reset/finalize!
cover_data = nil -- luacheck: no unused -- (which means its memory management is entirely in the hands of SQLite)
local cover_data, cover_size = zstd.zstd_uncompress_ctx(cover_blob[1], cover_blob[2])
-- Double-check that the size of the uncompressed BB is as expected...
local expected_cover_size = bbstride * bookinfo["cover_h"]
assert(cover_size == expected_cover_size, "Uncompressed a " .. tonumber(cover_size) .. "b BB instead of the expected " .. tonumber(expected_cover_size) .. "b")
-- That one, on the other hand, is on the heap, so we can use it without making a copy.
local cover_bb = Blitbuffer.new(bookinfo["cover_w"], bookinfo["cover_h"], bbtype, cover_data, bbstride, bookinfo["cover_w"])
-- Mark its data pointer as safe to free() on GC
cover_bb:setAllocated(1)
bookinfo["cover_bb"] = cover_bb
end end
break break
end end
end end
self.get_stmt:clearbind():reset() -- get ready for next query
return bookinfo return bookinfo
end end
@ -395,6 +444,11 @@ function BookInfoManager:extractBookInfo(filepath, cover_specs)
return -- Last insert done for this book, we're giving up return -- Last insert done for this book, we're giving up
end end
-- Update this on each extraction attempt. Might be useful to reset the counter in case file gets updated.
local file_attr = lfs.attributes(filepath)
dbrow.filesize = file_attr.size
dbrow.filemtime = file_attr.modification
-- Proceed with extracting info -- Proceed with extracting info
local document = DocumentRegistry:openDocument(filepath) local document = DocumentRegistry:openDocument(filepath)
local loaded = true local loaded = true
@ -423,7 +477,28 @@ function BookInfoManager:extractBookInfo(filepath, cover_specs)
end end
if props.title and props.title ~= "" then dbrow.title = props.title end if props.title and props.title ~= "" then dbrow.title = props.title end
if props.authors and props.authors ~= "" then dbrow.authors = props.authors end if props.authors and props.authors ~= "" then dbrow.authors = props.authors end
if props.series and props.series ~= "" then dbrow.series = props.series end if props.series and props.series ~= "" then
-- NOTE: If there's a series index in there, split it off to series_index, and only store the name in series.
-- This property is currently only set by:
-- * DjVu, for which I couldn't find a real standard for metadata fields
-- (we currently use Series for this field, c.f., https://exiftool.org/TagNames/DjVu.html).
-- * CRe, which could offer us a split getSeriesName & getSeriesNumber...
-- except getSeriesNumber does an atoi, so it'd murder decimal values.
-- So, instead, parse how it formats the whole thing as a string ;).
if string.find(props.series, "#") then
dbrow.series, dbrow.series_index = props.series:match("(.*) #(%d+%.?%d-)$")
if dbrow.series_index then
-- We're inserting via a bind method, so make sure we feed it a Lua number, because it's a REAL in the db.
dbrow.series_index = tonumber(dbrow.series_index)
else
-- If the index pattern didn't match (e.g., nothing after the octothorp, or a string),
-- restore the full thing as the series name.
dbrow.series = props.series
end
else
dbrow.series = props.series
end
end
if props.language and props.language ~= "" then dbrow.language = props.language end if props.language and props.language ~= "" then dbrow.language = props.language end
if props.keywords and props.keywords ~= "" then dbrow.keywords = props.keywords end if props.keywords and props.keywords ~= "" then dbrow.keywords = props.keywords end
if props.description and props.description ~= "" then dbrow.description = props.description end if props.description and props.description ~= "" then dbrow.description = props.description end
@ -447,18 +522,16 @@ function BookInfoManager:extractBookInfo(filepath, cover_specs)
cbb_h = math.min(math.floor(cbb_h * scale_factor)+1, spec_max_cover_h) cbb_h = math.min(math.floor(cbb_h * scale_factor)+1, spec_max_cover_h)
cover_bb = RenderImage:scaleBlitBuffer(cover_bb, cbb_w, cbb_h, true) cover_bb = RenderImage:scaleBlitBuffer(cover_bb, cbb_w, cbb_h, true)
end end
dbrow.cover_w = cbb_w dbrow.cover_w = cover_bb.w
dbrow.cover_h = cbb_h dbrow.cover_h = cover_bb.h
dbrow.cover_btype = cover_bb:getType() dbrow.cover_bb_type = cover_bb:getType()
dbrow.cover_bpitch = cover_bb.stride dbrow.cover_bb_stride = tonumber(cover_bb.stride)
local cover_data = Blitbuffer.tostring(cover_bb) local cover_size = cover_bb.stride * cover_bb.h
cover_bb:free() -- free bb before compressing to save memory local cover_zst_ptr, cover_zst_size = zstd.zstd_compress(cover_bb.data, cover_size)
dbrow.cover_datalen = cover_data:len() dbrow.cover_bb_data = SQ3.blob(cover_zst_ptr, cover_zst_size) -- cast to blob for sqlite
local cover_dataz = xutil.zlib_compress(cover_data) logger.dbg("cover for", filename, "scaled by", scale_factor, "=>", cover_bb.w, "x", cover_bb.h, ", compressed from", tonumber(cover_size), "to", tonumber(cover_zst_size))
-- release memory used by uncompressed data: -- We're done with the uncompressed bb now, and the compressed one has been managed by SQLite ;)
cover_data = nil -- luacheck: no unused cover_bb:free()
dbrow.cover_dataz = SQ3.blob(cover_dataz) -- cast to blob for sqlite
logger.dbg("cover for", filename, "scaled by", scale_factor, "=>", cbb_w, "x", cbb_h, ", compressed from", dbrow.cover_datalen, "to", cover_dataz:len())
end end
end end
end end
@ -684,7 +757,6 @@ end
-- Batch extraction -- Batch extraction
function BookInfoManager:extractBooksInDirectory(path, cover_specs) function BookInfoManager:extractBooksInDirectory(path, cover_specs)
local Geom = require("ui/geometry") local Geom = require("ui/geometry")
local InfoMessage = require("ui/widget/infomessage")
local TopContainer = require("ui/widget/container/topcontainer") local TopContainer = require("ui/widget/container/topcontainer")
local Trapper = require("ui/trapper") local Trapper = require("ui/trapper")
local Screen = require("device").screen local Screen = require("device").screen

@ -540,9 +540,11 @@ function ListMenuItem:update()
end end
-- add Series metadata if requested -- add Series metadata if requested
if bookinfo.series then if bookinfo.series then
-- Shorten calibre series decimal number (#4.0 => #4) if bookinfo.series_index then
bookinfo.series = bookinfo.series:gsub("(#%d+)%.0$", "%1") bookinfo.series = BD.auto(bookinfo.series .. " #" .. bookinfo.series_index)
bookinfo.series = BD.auto(bookinfo.series) else
bookinfo.series = BD.auto(bookinfo.series)
end
if series_mode == "append_series_to_title" then if series_mode == "append_series_to_title" then
if title then if title then
title = title .. " - " .. bookinfo.series title = title .. " - " .. bookinfo.series

@ -595,9 +595,11 @@ function MosaicMenuItem:update()
local series_mode = BookInfoManager:getSetting("series_mode") local series_mode = BookInfoManager:getSetting("series_mode")
local title_add, authors_add local title_add, authors_add
if bookinfo.series then if bookinfo.series then
-- Shorten calibre series decimal number (#4.0 => #4) if bookinfo.series_index then
bookinfo.series = bookinfo.series:gsub("(#%d+)%.0$", "%1") bookinfo.series = BD.auto(bookinfo.series .. " #" .. bookinfo.series_index)
bookinfo.series = BD.auto(bookinfo.series) else
bookinfo.series = BD.auto(bookinfo.series)
end
if series_mode == "append_series_to_title" then if series_mode == "append_series_to_title" then
if bookinfo.title then if bookinfo.title then
title_add = " - " .. bookinfo.series title_add = " - " .. bookinfo.series

@ -1,35 +0,0 @@
local ffi = require("ffi")
-- Utilities functions needed by this plugin, but that may be added to
-- existing base/ffi/ files
local xutil = {}
-- Data compression/decompression of strings thru zlib (may be put in a new base/ffi/zlib.lua)
-- from http://luajit.org/ext_ffi_tutorial.html
ffi.cdef[[
unsigned long compressBound(unsigned long sourceLen);
int compress2(uint8_t *dest, unsigned long *destLen,
const uint8_t *source, unsigned long sourceLen, int level);
int uncompress(uint8_t *dest, unsigned long *destLen,
const uint8_t *source, unsigned long sourceLen);
]]
local zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z")
function xutil.zlib_compress(data)
local n = zlib.compressBound(#data)
local buf = ffi.new("uint8_t[?]", n)
local buflen = ffi.new("unsigned long[1]", n)
local res = zlib.compress2(buf, buflen, data, #data, 9)
assert(res == 0)
return ffi.string(buf, buflen[0])
end
function xutil.zlib_uncompress(zdata, datalen)
local buf = ffi.new("uint8_t[?]", datalen)
local buflen = ffi.new("unsigned long[1]", datalen)
local res = zlib.uncompress(buf, buflen, zdata, #zdata)
assert(res == 0)
return ffi.string(buf, buflen[0])
end
return xutil

@ -53,7 +53,9 @@ if G_reader_settings:isTrue("debug") and G_reader_settings:isTrue("debug_verbose
-- Option parsing: -- Option parsing:
local longopts = { local longopts = {
debug = "d", debug = "d",
verbose = "d",
profile = "p", profile = "p",
teardown = "t",
help = "h", help = "h",
} }
@ -64,6 +66,7 @@ local function showusage()
print("-d start in debug mode") print("-d start in debug mode")
print("-v debug in verbose mode") print("-v debug in verbose mode")
print("-p enable Lua code profiling") print("-p enable Lua code profiling")
print("-t teardown via a return instead of calling os.exit")
print("-h show this usage help") print("-h show this usage help")
print("") print("")
print("If you give the name of a directory instead of a file path, a file") print("If you give the name of a directory instead of a file path, a file")
@ -76,6 +79,7 @@ local function showusage()
end end
local Profiler = nil local Profiler = nil
local sane_teardown
local ARGV = arg local ARGV = arg
local argidx = 1 local argidx = 1
while argidx <= #ARGV do while argidx <= #ARGV do
@ -97,6 +101,8 @@ while argidx <= #ARGV do
elseif arg == "-p" then elseif arg == "-p" then
Profiler = require("jit.p") Profiler = require("jit.p")
Profiler.start("la") Profiler.start("la")
elseif arg == "-t" then
sane_teardown = true
else else
-- not a recognized option, should be a filename -- not a recognized option, should be a filename
argidx = argidx - 1 argidx = argidx - 1
@ -335,10 +341,20 @@ local function exitReader()
if Profiler then Profiler.stop() end if Profiler then Profiler.stop() end
if type(exit_code) == "number" then if type(exit_code) == "number" then
os.exit(exit_code) return exit_code
else else
os.exit(0) return 0
end end
end end
exitReader() local ret = exitReader()
if not sane_teardown then
os.exit(ret)
else
-- NOTE: We can unfortunately not return with a custom exit code...
-- But since this should only really be used on the emulator,
-- to ensure a saner teardown of ressources when debugging,
-- it's not a great loss...
return
end

@ -10,6 +10,9 @@ describe("Cache module", function()
local sample_pdf = "spec/front/unit/data/sample.pdf" local sample_pdf = "spec/front/unit/data/sample.pdf"
doc = DocumentRegistry:openDocument(sample_pdf) doc = DocumentRegistry:openDocument(sample_pdf)
end) end)
teardown(function()
doc:close()
end)
it("should clear cache", function() it("should clear cache", function()
Cache:clear() Cache:clear()

@ -68,6 +68,9 @@ describe("Evernote plugin module", function()
} }
end) end)
teardown(function()
readerui:onClose()
end)
it("should write clippings to txt file", function () it("should write clippings to txt file", function ()
local file_mock = mock( { local file_mock = mock( {
@ -101,4 +104,4 @@ describe("Evernote plugin module", function()
end) end)
end) end)

@ -53,6 +53,10 @@ describe("ReaderBookmark module", function()
} }
readerui.status.enabled = false readerui.status.enabled = false
end) end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
before_each(function() before_each(function()
UIManager:quit() UIManager:quit()
UIManager:show(readerui) UIManager:show(readerui)
@ -136,6 +140,10 @@ describe("ReaderBookmark module", function()
} }
readerui.status.enabled = false readerui.status.enabled = false
end) end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
before_each(function() before_each(function()
UIManager:quit() UIManager:quit()
UIManager:show(readerui) UIManager:show(readerui)

@ -19,6 +19,10 @@ describe("Readerdictionary module", function()
rolling = readerui.rolling rolling = readerui.rolling
dictionary = readerui.dictionary dictionary = readerui.dictionary
end) end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
it("should show quick lookup window", function() it("should show quick lookup window", function()
local name = "screenshots/reader_dictionary.png" local name = "screenshots/reader_dictionary.png"
UIManager:quit() UIManager:quit()

@ -89,6 +89,8 @@ describe("Readerfooter module", function()
} }
assert.is.same(true, readerui.view.footer_visible) assert.is.same(true, readerui.view.footer_visible)
G_reader_settings:delSetting("reader_footer_mode") G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end) end)
it("should setup footer as visible not in all_at_once", function() it("should setup footer as visible not in all_at_once", function()
@ -116,6 +118,8 @@ describe("Readerfooter module", function()
assert.is.same(true, readerui.view.footer_visible) assert.is.same(true, readerui.view.footer_visible)
assert.is.same(1, readerui.view.footer.mode, 1) assert.is.same(1, readerui.view.footer.mode, 1)
G_reader_settings:delSetting("reader_footer_mode") G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end) end)
it("should setup footer as invisible in full screen mode", function() it("should setup footer as invisible in full screen mode", function()
@ -133,6 +137,8 @@ describe("Readerfooter module", function()
} }
assert.is.same(false, readerui.view.footer_visible) assert.is.same(false, readerui.view.footer_visible)
G_reader_settings:delSetting("reader_footer_mode") G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end) end)
it("should setup footer as visible in mini progress bar mode", function() it("should setup footer as visible in mini progress bar mode", function()
@ -150,6 +156,8 @@ describe("Readerfooter module", function()
} }
assert.is.same(false, readerui.view.footer_visible) assert.is.same(false, readerui.view.footer_visible)
G_reader_settings:delSetting("reader_footer_mode") G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end) end)
it("should setup footer as invisible", function() it("should setup footer as invisible", function()
@ -167,6 +175,8 @@ describe("Readerfooter module", function()
} }
assert.is.same(true, readerui.view.footer_visible) assert.is.same(true, readerui.view.footer_visible)
G_reader_settings:delSetting("reader_footer_mode") G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end) end)
it("should setup footer for epub without error", function() it("should setup footer for epub without error", function()
@ -186,6 +196,8 @@ describe("Readerfooter module", function()
-- c.f., NOTE above, Statistics are disabled, hence the N/A results -- c.f., NOTE above, Statistics are disabled, hence the N/A results
assert.are.same('1 / '..page_count..' | '..timeinfo..' | ⇒ 0 | 0% | ⤠ 0% | ⏳ N/A | ⤻ N/A', assert.are.same('1 / '..page_count..' | '..timeinfo..' | ⇒ 0 | 0% | ⤠ 0% | ⏳ N/A | ⤻ N/A',
footer.footer_text.text) footer.footer_text.text)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should setup footer for pdf without error", function() it("should setup footer for pdf without error", function()
@ -202,6 +214,8 @@ describe("Readerfooter module", function()
local timeinfo = readerui.view.footer.textGeneratorMap.time(footer) local timeinfo = readerui.view.footer.textGeneratorMap.time(footer)
assert.are.same('1 / 2 | '..timeinfo..' | ⇒ 1 | 0% | ⤠ 50% | ⏳ N/A | ⤻ N/A', assert.are.same('1 / 2 | '..timeinfo..' | ⇒ 1 | 0% | ⤠ 50% | ⏳ N/A | ⤻ N/A',
readerui.view.footer.footer_text.text) readerui.view.footer.footer_text.text)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should switch between different modes", function() it("should switch between different modes", function()
@ -257,6 +271,8 @@ describe("Readerfooter module", function()
-- reenable chapter time to read, text should be chapter time to read -- reenable chapter time to read, text should be chapter time to read
tapFooterMenu(fake_menu, "Chapter time to read".." (⤻)") tapFooterMenu(fake_menu, "Chapter time to read".." (⤻)")
assert.are.same('⤻ N/A', footer.footer_text.text) assert.are.same('⤻ N/A', footer.footer_text.text)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should rotate through different modes", function() it("should rotate through different modes", function()
@ -296,6 +312,8 @@ describe("Readerfooter module", function()
-- Make it visible again to make the following tests behave... -- Make it visible again to make the following tests behave...
footer:onTapFooter() footer:onTapFooter()
assert.is.same(1, footer.mode) assert.is.same(1, footer.mode)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should pick up screen resize in resetLayout", function() it("should pick up screen resize in resetLayout", function()
@ -330,6 +348,8 @@ describe("Readerfooter module", function()
expected = is_am() and 518 or 510 expected = is_am() and 518 or 510
assert.is.same(expected, footer.progress_bar.width) assert.is.same(expected, footer.progress_bar.width)
Screen.getWidth = old_screen_getwidth Screen.getWidth = old_screen_getwidth
readerui:closeDocument()
readerui:onClose()
end) end)
it("should update width on PosUpdate event", function() it("should update width on PosUpdate event", function()
@ -353,6 +373,8 @@ describe("Readerfooter module", function()
assert.are.same(expected, footer.progress_bar.width) assert.are.same(expected, footer.progress_bar.width)
expected = is_am() and 394 or 402 expected = is_am() and 394 or 402
assert.are.same(expected, footer.text_width) assert.are.same(expected, footer.text_width)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should support chapter markers", function() it("should support chapter markers", function()
@ -378,6 +400,8 @@ describe("Readerfooter module", function()
footer.settings.toc_markers = false footer.settings.toc_markers = false
footer:setTocMarkers() footer:setTocMarkers()
assert.are.same(nil, footer.progress_bar.ticks) assert.are.same(nil, footer.progress_bar.ticks)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should schedule/unschedule auto refresh time task", function() it("should schedule/unschedule auto refresh time task", function()
@ -412,6 +436,8 @@ describe("Readerfooter module", function()
end end
end end
assert.is.same(0, found) assert.is.same(0, found)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should not schedule auto refresh time task if footer is disabled", function() it("should not schedule auto refresh time task if footer is disabled", function()
@ -438,6 +464,8 @@ describe("Readerfooter module", function()
end end
end end
assert.is.same(0, found) assert.is.same(0, found)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should toggle auto refresh time task by toggling the menu", function() it("should toggle auto refresh time task by toggling the menu", function()
@ -487,6 +515,8 @@ describe("Readerfooter module", function()
end end
end end
assert.is.same(1, found) assert.is.same(1, found)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should support toggle footer through menu if tap zone is disabled", function() it("should support toggle footer through menu if tap zone is disabled", function()
@ -532,6 +562,8 @@ describe("Readerfooter module", function()
assert.is.same(2, footer.mode) assert.is.same(2, footer.mode)
DTAP_ZONE_MINIBAR = saved_tap_zone_minibar --luacheck: ignore DTAP_ZONE_MINIBAR = saved_tap_zone_minibar --luacheck: ignore
readerui:closeDocument()
readerui:onClose()
end) end)
it("should remove and add modes to footer text in all_at_once mode", function() it("should remove and add modes to footer text in all_at_once mode", function()
@ -563,6 +595,8 @@ describe("Readerfooter module", function()
-- add mode to footer text -- add mode to footer text
tapFooterMenu(fake_menu, "Progress percentage".." (⤠)") tapFooterMenu(fake_menu, "Progress percentage".." (⤠)")
assert.are.same('1 / 2 | ⤠ 50%', footer.footer_text.text) assert.are.same('1 / 2 | ⤠ 50%', footer.footer_text.text)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should initialize text mode in all_at_once mode", function() it("should initialize text mode in all_at_once mode", function()
@ -587,6 +621,8 @@ describe("Readerfooter module", function()
assert.is.truthy(footer.settings.all_at_once) assert.is.truthy(footer.settings.all_at_once)
assert.is.truthy(0, footer.mode) assert.is.truthy(0, footer.mode)
assert.is.falsy(readerui.view.footer_visible) assert.is.falsy(readerui.view.footer_visible)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should support disabling all the modes", function() it("should support disabling all the modes", function()
@ -624,6 +660,8 @@ describe("Readerfooter module", function()
assert.is.same(true, footer.has_no_mode) assert.is.same(true, footer.has_no_mode)
tapFooterMenu(fake_menu, "Progress percentage".." (⤠)") tapFooterMenu(fake_menu, "Progress percentage".." (⤠)")
assert.is.same(false, footer.has_no_mode) assert.is.same(false, footer.has_no_mode)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should return correct footer height in time mode", function() it("should return correct footer height in time mode", function()
@ -643,6 +681,8 @@ describe("Readerfooter module", function()
assert.falsy(footer.has_no_mode) assert.falsy(footer.has_no_mode)
assert.truthy(readerui.view.footer_visible) assert.truthy(readerui.view.footer_visible)
assert.is.same(15, footer:getHeight()) assert.is.same(15, footer:getHeight())
readerui:closeDocument()
readerui:onClose()
end) end)
it("should return correct footer height when all modes are disabled", function() it("should return correct footer height when all modes are disabled", function()
@ -662,6 +702,8 @@ describe("Readerfooter module", function()
assert.truthy(footer.has_no_mode) assert.truthy(footer.has_no_mode)
assert.truthy(readerui.view.footer_visible) assert.truthy(readerui.view.footer_visible)
assert.is.same(15, footer:getHeight()) assert.is.same(15, footer:getHeight())
readerui:closeDocument()
readerui:onClose()
end) end)
it("should disable footer when all modes + progressbar are disabled", function() it("should disable footer when all modes + progressbar are disabled", function()
@ -680,6 +722,8 @@ describe("Readerfooter module", function()
assert.truthy(footer.has_no_mode) assert.truthy(footer.has_no_mode)
assert.falsy(readerui.view.footer_visible) assert.falsy(readerui.view.footer_visible)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should disable footer if settings.disabled is true", function() it("should disable footer if settings.disabled is true", function()
@ -698,6 +742,8 @@ describe("Readerfooter module", function()
assert.falsy(readerui.view.footer_visible) assert.falsy(readerui.view.footer_visible)
assert.truthy(footer.onCloseDocument == nil) assert.truthy(footer.onCloseDocument == nil)
assert.truthy(footer.mode == 0) assert.truthy(footer.mode == 0)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should toggle between full and min progress bar for cre documents", function() it("should toggle between full and min progress bar for cre documents", function()
@ -723,5 +769,7 @@ describe("Readerfooter module", function()
readerui.rolling:onSetStatusLine(0) readerui.rolling:onSetStatusLine(0)
assert.is.same(0, footer.mode) assert.is.same(0, footer.mode)
assert.falsy(readerui.view.footer_visible) assert.falsy(readerui.view.footer_visible)
readerui:closeDocument()
readerui:onClose()
end) end)
end) end)

@ -74,6 +74,10 @@ describe("Readerhighlight module", function()
document = DocumentRegistry:openDocument(sample_epub), document = DocumentRegistry:openDocument(sample_epub),
} }
end) end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
before_each(function() before_each(function()
UIManager:quit() UIManager:quit()
readerui.rolling:onGotoPage(page) readerui.rolling:onGotoPage(page)
@ -117,6 +121,10 @@ describe("Readerhighlight module", function()
} }
readerui:handleEvent(Event:new("SetScrollMode", false)) readerui:handleEvent(Event:new("SetScrollMode", false))
end) end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
describe("for scanned page with text layer", function() describe("for scanned page with text layer", function()
before_each(function() before_each(function()
UIManager:quit() UIManager:quit()
@ -201,6 +209,10 @@ describe("Readerhighlight module", function()
} }
readerui:handleEvent(Event:new("SetScrollMode", true)) readerui:handleEvent(Event:new("SetScrollMode", true))
end) end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
describe("for scanned page with text layer", function() describe("for scanned page with text layer", function()
before_each(function() before_each(function()
UIManager:quit() UIManager:quit()

@ -22,6 +22,8 @@ describe("ReaderLink module", function()
readerui.rolling:onGotoPage(5) readerui.rolling:onGotoPage(5)
readerui.link:onTap(nil, {pos = {x = 320, y = 190}}) readerui.link:onTap(nil, {pos = {x = 320, y = 190}})
assert.is.same(37, readerui.rolling.current_page) assert.is.same(37, readerui.rolling.current_page)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should jump to links in pdf page mode", function() it("should jump to links in pdf page mode", function()
@ -36,6 +38,8 @@ describe("ReaderLink module", function()
readerui.link:onTap(nil, {pos = {x = 363, y = 565}}) readerui.link:onTap(nil, {pos = {x = 363, y = 565}})
UIManager:run() UIManager:run()
assert.is.same(22, readerui.paging.current_page) assert.is.same(22, readerui.paging.current_page)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should jump to links in pdf scroll mode", function() it("should jump to links in pdf scroll mode", function()
@ -54,6 +58,8 @@ describe("ReaderLink module", function()
-- page positions may have unexpected impact on page number -- page positions may have unexpected impact on page number
assert.truthy(readerui.paging.current_page == 21 assert.truthy(readerui.paging.current_page == 21
or readerui.paging.current_page == 20) or readerui.paging.current_page == 20)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should be able to go back after link jump in epub #nocov", function() it("should be able to go back after link jump in epub #nocov", function()
@ -66,6 +72,8 @@ describe("ReaderLink module", function()
assert.is.same(37, readerui.rolling.current_page) assert.is.same(37, readerui.rolling.current_page)
readerui.link:onGoBackLink() readerui.link:onGoBackLink()
assert.is.same(5, readerui.rolling.current_page) assert.is.same(5, readerui.rolling.current_page)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should be able to go back after link jump in pdf page mode", function() it("should be able to go back after link jump in pdf page mode", function()
@ -82,6 +90,8 @@ describe("ReaderLink module", function()
assert.is.same(22, readerui.paging.current_page) assert.is.same(22, readerui.paging.current_page)
readerui.link:onGoBackLink() readerui.link:onGoBackLink()
assert.is.same(1, readerui.paging.current_page) assert.is.same(1, readerui.paging.current_page)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should be able to go back after link jump in pdf scroll mode", function() it("should be able to go back after link jump in pdf scroll mode", function()
@ -100,6 +110,8 @@ describe("ReaderLink module", function()
or readerui.paging.current_page == 20) or readerui.paging.current_page == 20)
readerui.link:onGoBackLink() readerui.link:onGoBackLink()
assert.is.same(1, readerui.paging.current_page) assert.is.same(1, readerui.paging.current_page)
readerui:closeDocument()
readerui:onClose()
end) end)
it("should be able to go back to the same position after link jump in pdf scroll mode", function() it("should be able to go back to the same position after link jump in pdf scroll mode", function()
@ -175,5 +187,7 @@ describe("ReaderLink module", function()
readerui.link:onGoBackLink() readerui.link:onGoBackLink()
assert.is.same(3, readerui.paging.current_page) assert.is.same(3, readerui.paging.current_page)
assert.are.same(expected_page_states, readerui.view.page_states) assert.are.same(expected_page_states, readerui.view.page_states)
readerui:closeDocument()
readerui:onClose()
end) end)
end) end)

@ -20,6 +20,10 @@ describe("Readerpaging module", function()
} }
paging = readerui.paging paging = readerui.paging
end) end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
it("should emit EndOfBook event at the end", function() it("should emit EndOfBook event at the end", function()
UIManager:quit() UIManager:quit()
@ -53,6 +57,10 @@ describe("Readerpaging module", function()
} }
paging = readerui.paging paging = readerui.paging
end) end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
it("should emit EndOfBook event at the end", function() it("should emit EndOfBook event at the end", function()
UIManager:quit() UIManager:quit()
@ -81,6 +89,8 @@ describe("Readerpaging module", function()
document = DocumentRegistry:openDocument(sample_djvu), document = DocumentRegistry:openDocument(sample_djvu),
} }
tmp_readerui.paging:onScrollPanRel(-100) tmp_readerui.paging:onScrollPanRel(-100)
tmp_readerui:closeDocument()
tmp_readerui:onClose()
end) end)
it("should scroll forward on the last page without crash", function() it("should scroll forward on the last page without crash", function()
@ -94,6 +104,8 @@ describe("Readerpaging module", function()
paging:onScrollPanRel(120) paging:onScrollPanRel(120)
paging:onScrollPanRel(-1) paging:onScrollPanRel(-1)
paging:onScrollPanRel(120) paging:onScrollPanRel(120)
tmp_readerui:closeDocument()
tmp_readerui:onClose()
end) end)
end) end)
end) end)

@ -106,6 +106,8 @@ describe("Readerrolling module", function()
txt_rolling:onGotoViewRel(1) txt_rolling:onGotoViewRel(1)
assert.is.truthy(called) assert.is.truthy(called)
readerui.onEndOfBook = nil readerui.onEndOfBook = nil
txt_readerui:closeDocument()
txt_readerui:onClose()
end) end)
end) end)
@ -205,10 +207,14 @@ describe("Readerrolling module", function()
end end
local test_book = "spec/front/unit/data/sample.txt" local test_book = "spec/front/unit/data/sample.txt"
require("docsettings"):open(test_book):purge() require("docsettings"):open(test_book):purge()
ReaderUI:new{ local tmp_readerui = ReaderUI:new{
document = DocumentRegistry:openDocument(test_book), document = DocumentRegistry:openDocument(test_book),
} }
ReaderView.onPageUpdate = saved_handler ReaderView.onPageUpdate = saved_handler
tmp_readerui:closeDocument()
tmp_readerui:onClose()
readerui:closeDocument()
readerui:onClose()
end) end)
end) end)
end) end)

@ -12,9 +12,9 @@ describe("Readersearch module", function()
end) end)
describe("search API for EPUB documents", function() describe("search API for EPUB documents", function()
local doc, search, rolling local readerui, doc, search, rolling
setup(function() setup(function()
local readerui = ReaderUI:new{ readerui = ReaderUI:new{
dimen = Screen:getSize(), dimen = Screen:getSize(),
document = DocumentRegistry:openDocument(sample_epub), document = DocumentRegistry:openDocument(sample_epub),
} }
@ -22,6 +22,10 @@ describe("Readersearch module", function()
search = readerui.search search = readerui.search
rolling = readerui.rolling rolling = readerui.rolling
end) end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
it("should search backward", function() it("should search backward", function()
rolling:onGotoPage(10) rolling:onGotoPage(10)
assert.truthy(search:searchFromCurrent("Verona", 1)) assert.truthy(search:searchFromCurrent("Verona", 1))
@ -117,9 +121,9 @@ describe("Readersearch module", function()
end) end)
describe("search API for PDF documents", function() describe("search API for PDF documents", function()
local doc, search, paging local readerui, doc, search, paging
setup(function() setup(function()
local readerui = ReaderUI:new{ readerui = ReaderUI:new{
dimen = Screen:getSize(), dimen = Screen:getSize(),
document = DocumentRegistry:openDocument(sample_pdf), document = DocumentRegistry:openDocument(sample_pdf),
} }
@ -127,6 +131,10 @@ describe("Readersearch module", function()
search = readerui.search search = readerui.search
paging = readerui.paging paging = readerui.paging
end) end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
it("should match single word with case insensitive option in one page", function() it("should match single word with case insensitive option in one page", function()
assert.are.equal(9, #doc.koptinterface:findAllMatches(doc, "what", true, 20)) assert.are.equal(9, #doc.koptinterface:findAllMatches(doc, "what", true, 20))
assert.are.equal(51, #doc.koptinterface:findAllMatches(doc, "the", true, 20)) assert.are.equal(51, #doc.koptinterface:findAllMatches(doc, "the", true, 20))

@ -10,6 +10,12 @@ describe("Readertoc module", function()
DEBUG = require("dbg") DEBUG = require("dbg")
local sample_epub = "spec/front/unit/data/juliet.epub" local sample_epub = "spec/front/unit/data/juliet.epub"
-- Clear settings from previous tests
local DocSettings = require("docsettings")
local doc_settings = DocSettings:open(sample_epub)
doc_settings:close()
doc_settings:purge()
readerui = ReaderUI:new{ readerui = ReaderUI:new{
dimen = Screen:getSize(), dimen = Screen:getSize(),
document = DocumentRegistry:openDocument(sample_epub), document = DocumentRegistry:openDocument(sample_epub),
@ -97,6 +103,10 @@ describe("Readertoc module", function()
assert.are.same(12, #toc.collapsed_toc) assert.are.same(12, #toc.collapsed_toc)
toc:collapseToc(18) toc:collapseToc(18)
assert.are.same(7, #toc.collapsed_toc) assert.are.same(7, #toc.collapsed_toc)
--- @note: Delay the teardown 'til the last test, because of course the tests rely on incremental state changes across tests...
readerui:closeDocument()
readerui:onClose()
end) end)
end) end)
end) end)

@ -36,6 +36,7 @@ describe("Readerui module", function()
it("should close document", function() it("should close document", function()
readerui:closeDocument() readerui:closeDocument()
assert(readerui.document == nil) assert(readerui.document == nil)
readerui:onClose()
end) end)
it("should not reset running_instance by mistake", function() it("should not reset running_instance by mistake", function()
ReaderUI:doShowReader(sample_epub) ReaderUI:doShowReader(sample_epub)
@ -46,6 +47,7 @@ describe("Readerui module", function()
document = DocumentRegistry:openDocument(sample_epub) document = DocumentRegistry:openDocument(sample_epub)
}:onClose() }:onClose()
assert.is.truthy(new_readerui.document) assert.is.truthy(new_readerui.document)
new_readerui:closeDocument()
new_readerui:onClose() new_readerui:onClose()
end) end)
end) end)

@ -47,6 +47,10 @@ describe("Readerview module", function()
error("UIManager's task queue should be emtpy.") error("UIManager's task queue should be emtpy.")
end end
end end
if readerui.document then
readerui:closeDocument()
end
end) end)
it("should return and restore view context in page mode", function() it("should return and restore view context in page mode", function()
@ -99,6 +103,8 @@ describe("Readerview module", function()
assert.is.same(view.visible_area.x, 0) assert.is.same(view.visible_area.x, 0)
assert.is.same(view.visible_area.y, 10) assert.is.same(view.visible_area.y, 10)
G_reader_settings:delSetting("reader_footer_mode") G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end) end)
it("should return and restore view context in scroll mode", function() it("should return and restore view context in scroll mode", function()
@ -152,5 +158,7 @@ describe("Readerview module", function()
assert.is.same(view.page_states[1].visible_area.x, 0) assert.is.same(view.page_states[1].visible_area.x, 0)
assert.is.same(view.page_states[1].visible_area.y, 10) assert.is.same(view.page_states[1].visible_area.y, 10)
G_reader_settings:delSetting("reader_footer_mode") G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end) end)
end) end)

@ -19,6 +19,8 @@ describe("ReaderScreenshot module", function()
teardown(function() teardown(function()
readerui:handleEvent(Event:new("SetRotationMode", Screen.ORIENTATION_PORTRAIT)) readerui:handleEvent(Event:new("SetRotationMode", Screen.ORIENTATION_PORTRAIT))
readerui:closeDocument()
readerui:onClose()
end) end)
it("should get screenshot in portrait", function() it("should get screenshot in portrait", function()

Loading…
Cancel
Save