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
# for verbose_print module
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
# 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 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
if series == "" or series == nil then
series = _("N/A")
else -- Shorten calibre series decimal number (#4.0 => #4)
series = series:gsub("(#%d+)%.0$", "%1")
else
-- 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
table.insert(kv_pairs, { _("Series:"), BD.auto(series) })

@ -125,13 +125,6 @@ function KindlePowerD:isChargingHW()
return is_charging == 1
end
function KindlePowerD:__gc()
if self.lipc_handle then
self.lipc_handle:close()
self.lipc_handle = nil
end
end
function KindlePowerD:_readFLIntensity()
return self:read_int_file(self.fl_intensity_file)
end
@ -161,4 +154,12 @@ function KindlePowerD:toggleSuspend()
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

@ -15,7 +15,7 @@ end
function TileCacheItem:dump(filename)
logger.dbg("dumping tile cache to", filename, self.excerpt)
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)
end

@ -102,7 +102,7 @@ function RenderImage:renderGifImageDataWithGifLib(data, size, want_frames, width
if want_frames and nb_frames > 1 then
-- Returns a regular table, with functions (returning the BlitBuffer)
-- 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)
local frames = {}
-- 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
-- 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
-- Should happen with that, but __gc seems never called...
frames = setmetatable(frames, {
__gc = function()
logger.dbg("frames.gc() called, closing GifDocument")
if frames.gif_close_needed then
gif:close()
frames.gif_close_needed = nil
end
-- Since frames is a plain table, __gc won't work on Lua 5.1/LuaJIT,
-- not without a little help from the newproxy hack...
frames.gif = gif
local frames_mt = {}
function frames_mt:__gc()
logger.dbg("frames.gc() called, closing GifDocument", self.gif)
if self.gif_close_needed then
self.gif:close()
self.gif_close_needed = nil
end
})
-- so, also set this method, so that ImageViewer can explicitely
-- call it onClose.
frames.free = function()
logger.dbg("frames.free() called, closing GifDocument")
if frames.gif_close_needed then
gif:close()
frames.gif_close_needed = nil
end
-- Much like our other stuff, when we're puzzled about __gc, we do it manually!
-- So, also set this method, so that ImageViewer can explicitely call it onClose.
function frames:free()
logger.dbg("frames.free() called, closing GifDocument", self.gif)
if self.gif_close_needed then
self.gif:close()
self.gif_close_needed = nil
end
end
local setmetatable = require("ffi/__gc")
setmetatable(frames, frames_mt)
return frames
else
local page = gif:openPage(1)

@ -719,7 +719,7 @@ function ImageViewer:onCloseWidget()
end
-- 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
self._images_list.free()
self._images_list:free()
end
-- NOTE: Assume there's no image beneath us, so, no dithering request
UIManager:setDirty(nil, function()

@ -205,7 +205,7 @@ function ImageWidget:_loadfile()
-- cache this image
logger.dbg("cache", hash)
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)
end
end

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

@ -4,36 +4,37 @@ local DataStorage = require("datastorage")
local Device = require("device")
local DocumentRegistry = require("document/documentregistry")
local FFIUtil = require("ffi/util")
local InfoMessage = require("ui/widget/infomessage")
local RenderImage = require("ui/renderimage")
local SQ3 = require("lua-ljsqlite3/init")
local UIManager = require("ui/uimanager")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("util")
local zstd = require("ffi/zstd")
local _ = require("gettext")
local N_ = _.ngettext
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
local BOOKINFO_DB_VERSION = "2-20170701"
local BOOKINFO_DB_VERSION = 20201210
local BOOKINFO_DB_SCHEMA = [[
-- For caching book cover and metadata
-- To cache book cover and metadata
CREATE TABLE IF NOT EXISTS bookinfo (
-- 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,
-- File location and filename
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
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
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_cover TEXT, -- NULL / 'Y' = has cover image (cover_*)
cover_sizetag TEXT, -- 'M' (Medium, MosaicMenuItem) / 's' (small, ListMenuItem)
@ -50,6 +51,7 @@ local BOOKINFO_DB_SCHEMA = [[
title TEXT,
authors TEXT,
series TEXT,
series_index REAL,
language TEXT,
keywords TEXT,
description TEXT,
@ -57,26 +59,24 @@ local BOOKINFO_DB_SCHEMA = [[
-- Cover image
cover_w INTEGER, -- blitbuffer width
cover_h INTEGER, -- blitbuffer height
cover_btype INTEGER, -- blitbuffer type (internal)
cover_bpitch INTEGER, -- blitbuffer pitch (internal)
cover_datalen INTEGER, -- blitbuffer uncompressed data length
cover_dataz BLOB -- blitbuffer data compressed with zlib
cover_bb_type INTEGER, -- blitbuffer type (internal)
cover_bb_stride INTEGER, -- blitbuffer stride (internal)
cover_bb_data BLOB -- blitbuffer data compressed with zstd
);
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 (
key TEXT PRIMARY KEY,
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 = {
"directory",
"filename",
"filesize",
"filemtime",
"in_progress",
"unsupported",
"cover_fetched",
@ -89,15 +89,15 @@ local BOOKINFO_COLS_SET = {
"title",
"authors",
"series",
"series_index",
"language",
"keywords",
"description",
"cover_w",
"cover_h",
"cover_btype",
"cover_bpitch",
"cover_datalen",
"cover_dataz",
"cover_bb_type",
"cover_bb_stride",
"cover_bb_data",
}
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
-- Create it (noop if already there)
db_conn:exec(BOOKINFO_DB_SCHEMA)
-- Check version (not updated by previous exec if already there)
local res = db_conn:exec("SELECT value FROM config where key='version';")
if res[1][1] ~= BOOKINFO_DB_VERSION then
logger.warn("BookInfo cache DB schema updated from version", res[1][1], "to version", BOOKINFO_DB_VERSION)
-- Check version
local db_version = tonumber(db_conn:rowexec("PRAGMA user_version;"))
if db_version < BOOKINFO_DB_VERSION then
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")
-- We'll try to preserve settings, though
self:loadSettings(db_conn)
db_conn:close()
os.remove(self.db_location)
-- Re-create it
db_conn = SQ3.open(self.db_location)
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
db_conn:close()
self.db_created = true
@ -216,19 +235,29 @@ function BookInfoManager:compactDb()
end
-- Settings management, stored in 'config' table
function BookInfoManager:loadSettings()
function BookInfoManager:loadSettings(db_conn)
if lfs.attributes(self.db_location, "mode") ~= "file" then
-- no db, empty config
self.settings = {}
return
end
self.settings = {}
self:openDbConnection()
local res = self.db_conn:exec("SELECT key, value FROM config;")
local keys = res[1]
local values = res[2]
for i, key in ipairs(keys) do
self.settings[key] = values[i]
local my_db_conn
if db_conn then
my_db_conn = db_conn
else
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
@ -239,7 +268,7 @@ function BookInfoManager:getSetting(key)
return self.settings[key]
end
function BookInfoManager:saveSetting(key, value)
function BookInfoManager:saveSetting(key, value, skip_reload)
if not value or value == false or value == "" then
if lfs.attributes(self.db_location, "mode") ~= "file" then
-- 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:step() -- commited
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
-- Bookinfo management
@ -274,6 +306,10 @@ function BookInfoManager:getBookInfo(filepath, get_cover)
return {
directory = directory,
filename = filename,
--[[
filesize = lfs.attributes(filepath, "size"),
filemtime = lfs.attributes(filepath, "modification"),
--]]
in_progress = 0,
cover_fetched = "Y",
has_meta = nil,
@ -289,9 +325,11 @@ function BookInfoManager:getBookInfo(filepath, get_cover)
self:openDbConnection()
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
self.get_stmt:clearbind():reset() -- get ready for next query
return nil
end
@ -313,16 +351,27 @@ function BookInfoManager:getBookInfo(filepath, get_cover)
if bookinfo["has_cover"] then
bookinfo["cover_w"] = tonumber(row[num])
bookinfo["cover_h"] = tonumber(row[num+1])
local cover_data = xutil.zlib_uncompress(row[num+5], row[num+4])
row[num+5] = nil -- release memory used by cover_dataz
-- Blitbuffer.fromstring() expects : w, h, bb_type, bb_data, pitch
bookinfo["cover_bb"] = Blitbuffer.fromstring(row[num], row[num+1], row[num+2], cover_data, row[num+3])
-- release memory used by uncompressed data:
cover_data = nil -- luacheck: no unused
local bbtype = tonumber(row[num+2])
local bbstride = tonumber(row[num+3])
-- This is a blob_mt table! Essentially, a (ptr, size) tuple.
local cover_blob = row[num+4]
-- The pointer returned by SQLite is only valid until the next step/reset/finalize!
-- (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
break
end
end
self.get_stmt:clearbind():reset() -- get ready for next query
return bookinfo
end
@ -395,6 +444,11 @@ function BookInfoManager:extractBookInfo(filepath, cover_specs)
return -- Last insert done for this book, we're giving up
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
local document = DocumentRegistry:openDocument(filepath)
local loaded = true
@ -423,7 +477,28 @@ function BookInfoManager:extractBookInfo(filepath, cover_specs)
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.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.keywords and props.keywords ~= "" then dbrow.keywords = props.keywords 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)
cover_bb = RenderImage:scaleBlitBuffer(cover_bb, cbb_w, cbb_h, true)
end
dbrow.cover_w = cbb_w
dbrow.cover_h = cbb_h
dbrow.cover_btype = cover_bb:getType()
dbrow.cover_bpitch = cover_bb.stride
local cover_data = Blitbuffer.tostring(cover_bb)
cover_bb:free() -- free bb before compressing to save memory
dbrow.cover_datalen = cover_data:len()
local cover_dataz = xutil.zlib_compress(cover_data)
-- release memory used by uncompressed data:
cover_data = nil -- luacheck: no unused
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())
dbrow.cover_w = cover_bb.w
dbrow.cover_h = cover_bb.h
dbrow.cover_bb_type = cover_bb:getType()
dbrow.cover_bb_stride = tonumber(cover_bb.stride)
local cover_size = cover_bb.stride * cover_bb.h
local cover_zst_ptr, cover_zst_size = zstd.zstd_compress(cover_bb.data, cover_size)
dbrow.cover_bb_data = SQ3.blob(cover_zst_ptr, cover_zst_size) -- cast to blob for sqlite
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))
-- We're done with the uncompressed bb now, and the compressed one has been managed by SQLite ;)
cover_bb:free()
end
end
end
@ -684,7 +757,6 @@ end
-- Batch extraction
function BookInfoManager:extractBooksInDirectory(path, cover_specs)
local Geom = require("ui/geometry")
local InfoMessage = require("ui/widget/infomessage")
local TopContainer = require("ui/widget/container/topcontainer")
local Trapper = require("ui/trapper")
local Screen = require("device").screen

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

@ -595,9 +595,11 @@ function MosaicMenuItem:update()
local series_mode = BookInfoManager:getSetting("series_mode")
local title_add, authors_add
if bookinfo.series then
-- Shorten calibre series decimal number (#4.0 => #4)
bookinfo.series = bookinfo.series:gsub("(#%d+)%.0$", "%1")
bookinfo.series = BD.auto(bookinfo.series)
if bookinfo.series_index then
bookinfo.series = BD.auto(bookinfo.series .. " #" .. bookinfo.series_index)
else
bookinfo.series = BD.auto(bookinfo.series)
end
if series_mode == "append_series_to_title" then
if bookinfo.title then
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:
local longopts = {
debug = "d",
verbose = "d",
profile = "p",
teardown = "t",
help = "h",
}
@ -64,6 +66,7 @@ local function showusage()
print("-d start in debug mode")
print("-v debug in verbose mode")
print("-p enable Lua code profiling")
print("-t teardown via a return instead of calling os.exit")
print("-h show this usage help")
print("")
print("If you give the name of a directory instead of a file path, a file")
@ -76,6 +79,7 @@ local function showusage()
end
local Profiler = nil
local sane_teardown
local ARGV = arg
local argidx = 1
while argidx <= #ARGV do
@ -97,6 +101,8 @@ while argidx <= #ARGV do
elseif arg == "-p" then
Profiler = require("jit.p")
Profiler.start("la")
elseif arg == "-t" then
sane_teardown = true
else
-- not a recognized option, should be a filename
argidx = argidx - 1
@ -335,10 +341,20 @@ local function exitReader()
if Profiler then Profiler.stop() end
if type(exit_code) == "number" then
os.exit(exit_code)
return exit_code
else
os.exit(0)
return 0
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"
doc = DocumentRegistry:openDocument(sample_pdf)
end)
teardown(function()
doc:close()
end)
it("should clear cache", function()
Cache:clear()

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

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

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

@ -89,6 +89,8 @@ describe("Readerfooter module", function()
}
assert.is.same(true, readerui.view.footer_visible)
G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end)
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(1, readerui.view.footer.mode, 1)
G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end)
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)
G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end)
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)
G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end)
it("should setup footer as invisible", function()
@ -167,6 +175,8 @@ describe("Readerfooter module", function()
}
assert.is.same(true, readerui.view.footer_visible)
G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end)
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
assert.are.same('1 / '..page_count..' | '..timeinfo..' | ⇒ 0 | 0% | ⤠ 0% | ⏳ N/A | ⤻ N/A',
footer.footer_text.text)
readerui:closeDocument()
readerui:onClose()
end)
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)
assert.are.same('1 / 2 | '..timeinfo..' | ⇒ 1 | 0% | ⤠ 50% | ⏳ N/A | ⤻ N/A',
readerui.view.footer.footer_text.text)
readerui:closeDocument()
readerui:onClose()
end)
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
tapFooterMenu(fake_menu, "Chapter time to read".." (⤻)")
assert.are.same('⤻ N/A', footer.footer_text.text)
readerui:closeDocument()
readerui:onClose()
end)
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...
footer:onTapFooter()
assert.is.same(1, footer.mode)
readerui:closeDocument()
readerui:onClose()
end)
it("should pick up screen resize in resetLayout", function()
@ -330,6 +348,8 @@ describe("Readerfooter module", function()
expected = is_am() and 518 or 510
assert.is.same(expected, footer.progress_bar.width)
Screen.getWidth = old_screen_getwidth
readerui:closeDocument()
readerui:onClose()
end)
it("should update width on PosUpdate event", function()
@ -353,6 +373,8 @@ describe("Readerfooter module", function()
assert.are.same(expected, footer.progress_bar.width)
expected = is_am() and 394 or 402
assert.are.same(expected, footer.text_width)
readerui:closeDocument()
readerui:onClose()
end)
it("should support chapter markers", function()
@ -378,6 +400,8 @@ describe("Readerfooter module", function()
footer.settings.toc_markers = false
footer:setTocMarkers()
assert.are.same(nil, footer.progress_bar.ticks)
readerui:closeDocument()
readerui:onClose()
end)
it("should schedule/unschedule auto refresh time task", function()
@ -412,6 +436,8 @@ describe("Readerfooter module", function()
end
end
assert.is.same(0, found)
readerui:closeDocument()
readerui:onClose()
end)
it("should not schedule auto refresh time task if footer is disabled", function()
@ -438,6 +464,8 @@ describe("Readerfooter module", function()
end
end
assert.is.same(0, found)
readerui:closeDocument()
readerui:onClose()
end)
it("should toggle auto refresh time task by toggling the menu", function()
@ -487,6 +515,8 @@ describe("Readerfooter module", function()
end
end
assert.is.same(1, found)
readerui:closeDocument()
readerui:onClose()
end)
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)
DTAP_ZONE_MINIBAR = saved_tap_zone_minibar --luacheck: ignore
readerui:closeDocument()
readerui:onClose()
end)
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
tapFooterMenu(fake_menu, "Progress percentage".." (⤠)")
assert.are.same('1 / 2 | ⤠ 50%', footer.footer_text.text)
readerui:closeDocument()
readerui:onClose()
end)
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(0, footer.mode)
assert.is.falsy(readerui.view.footer_visible)
readerui:closeDocument()
readerui:onClose()
end)
it("should support disabling all the modes", function()
@ -624,6 +660,8 @@ describe("Readerfooter module", function()
assert.is.same(true, footer.has_no_mode)
tapFooterMenu(fake_menu, "Progress percentage".." (⤠)")
assert.is.same(false, footer.has_no_mode)
readerui:closeDocument()
readerui:onClose()
end)
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.truthy(readerui.view.footer_visible)
assert.is.same(15, footer:getHeight())
readerui:closeDocument()
readerui:onClose()
end)
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(readerui.view.footer_visible)
assert.is.same(15, footer:getHeight())
readerui:closeDocument()
readerui:onClose()
end)
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.falsy(readerui.view.footer_visible)
readerui:closeDocument()
readerui:onClose()
end)
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.truthy(footer.onCloseDocument == nil)
assert.truthy(footer.mode == 0)
readerui:closeDocument()
readerui:onClose()
end)
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)
assert.is.same(0, footer.mode)
assert.falsy(readerui.view.footer_visible)
readerui:closeDocument()
readerui:onClose()
end)
end)

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

@ -22,6 +22,8 @@ describe("ReaderLink module", function()
readerui.rolling:onGotoPage(5)
readerui.link:onTap(nil, {pos = {x = 320, y = 190}})
assert.is.same(37, readerui.rolling.current_page)
readerui:closeDocument()
readerui:onClose()
end)
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}})
UIManager:run()
assert.is.same(22, readerui.paging.current_page)
readerui:closeDocument()
readerui:onClose()
end)
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
assert.truthy(readerui.paging.current_page == 21
or readerui.paging.current_page == 20)
readerui:closeDocument()
readerui:onClose()
end)
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)
readerui.link:onGoBackLink()
assert.is.same(5, readerui.rolling.current_page)
readerui:closeDocument()
readerui:onClose()
end)
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)
readerui.link:onGoBackLink()
assert.is.same(1, readerui.paging.current_page)
readerui:closeDocument()
readerui:onClose()
end)
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)
readerui.link:onGoBackLink()
assert.is.same(1, readerui.paging.current_page)
readerui:closeDocument()
readerui:onClose()
end)
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()
assert.is.same(3, readerui.paging.current_page)
assert.are.same(expected_page_states, readerui.view.page_states)
readerui:closeDocument()
readerui:onClose()
end)
end)

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

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

@ -12,9 +12,9 @@ describe("Readersearch module", function()
end)
describe("search API for EPUB documents", function()
local doc, search, rolling
local readerui, doc, search, rolling
setup(function()
local readerui = ReaderUI:new{
readerui = ReaderUI:new{
dimen = Screen:getSize(),
document = DocumentRegistry:openDocument(sample_epub),
}
@ -22,6 +22,10 @@ describe("Readersearch module", function()
search = readerui.search
rolling = readerui.rolling
end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
it("should search backward", function()
rolling:onGotoPage(10)
assert.truthy(search:searchFromCurrent("Verona", 1))
@ -117,9 +121,9 @@ describe("Readersearch module", function()
end)
describe("search API for PDF documents", function()
local doc, search, paging
local readerui, doc, search, paging
setup(function()
local readerui = ReaderUI:new{
readerui = ReaderUI:new{
dimen = Screen:getSize(),
document = DocumentRegistry:openDocument(sample_pdf),
}
@ -127,6 +131,10 @@ describe("Readersearch module", function()
search = readerui.search
paging = readerui.paging
end)
teardown(function()
readerui:closeDocument()
readerui:onClose()
end)
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(51, #doc.koptinterface:findAllMatches(doc, "the", true, 20))

@ -10,6 +10,12 @@ describe("Readertoc module", function()
DEBUG = require("dbg")
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{
dimen = Screen:getSize(),
document = DocumentRegistry:openDocument(sample_epub),
@ -97,6 +103,10 @@ describe("Readertoc module", function()
assert.are.same(12, #toc.collapsed_toc)
toc:collapseToc(18)
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)

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

@ -47,6 +47,10 @@ describe("Readerview module", function()
error("UIManager's task queue should be emtpy.")
end
end
if readerui.document then
readerui:closeDocument()
end
end)
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.y, 10)
G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end)
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.y, 10)
G_reader_settings:delSetting("reader_footer_mode")
readerui:closeDocument()
readerui:onClose()
end)
end)

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

Loading…
Cancel
Save