koreader
koreader copied to clipboard
Modifiy Fit to full + zoom to page : stretch so it uses the whole screen
Does your feature request involve difficulty completing a task?
- I Open a CBZ jpeg/webP with MUpdf, and configure : zoom to "page" or "content" (same effect) / fit to "full" or "width". But it keeps the picture ratio. There seems to be no option to stretch the picture to fill the whole screen.
Actual configuration :
- Fit to "full" = Full screen with picture original aspect ratio
- Fit to "width" + zoom to "content" = Full screen with picture original ratio (need scroll).
- Fit to "width" + zoom to "page" = Full screen with picture original ratio (need scroll).
I would like to have choice to keep ratio or stretch the image : to get a real fullscreen without scrolling on the same picture.
Describe the solution you'd like
- Modify zoom mode option : "Fit to width" + "zoom to page" = stretch to the screen (without scroll)
Describe alternatives you've considered
Code edit with Gemini AI to get a stretch fit :
- mupdf.lua
- readerpaging.lua
Modified configuration :
- Fit to "full" = Full screen with picture original aspect ratio
- Fit to "width" + zoom to "content" = Full screen with picture stretched (no scroll).
- Fit to "width" + zoom to "page" = Full screen with picture stretched (no scroll).
I would like an option to chose between :
- Fit to "full" = Full screen with picture original aspect ratio
- Fit to "width" + zoom to "content" = Full screen with picture original ratio (need scroll).
- Fit to "width" + zoom to "page" = Full screen with picture stretched (no scroll).
Additional context
- I use a Kobo Libra Colour (7" screen) : 1264x1680.
- Attached below, modifed configuration :
- mupdf.lua
- readerpaging.lua
mupdf.lua
--[[--
MuPDF API
This is a FFI wrapper for what was a Lua-based API in the past
Some kind of C wrapper is needed for muPDF since muPDF uses
a setjmp/longjmp based approach to error/exception handling.
That's one of the very few things we can't deal with using
LuaJIT's FFI.
@module ffi.mupdf
--]]
local ffi = require("ffi")
local Device = require("device") -- Ajout Gemini pour récupérer la taille écran
local Screen = Device.screen -- Ajout Gemini
require("ffi/mupdf_h")
require("ffi/posix_h") -- for malloc
local BlitBuffer = require("ffi/blitbuffer")
local C = ffi.C
local W = ffi.loadlib("wrap-mupdf")
local M = W
--- @fixme: Don't make cache_size too low, at least not until we bump MµPDF,
--- as there's a pernicious issue that corrupts its store cache on overcommit on old versions.
--- c.f., https://github.com/koreader/koreader/issues/7627
--- (FZ_STORE_DEFAULT is 256MB, we used to set it to 8MB).
--- And when we bump MµPDF, it'll likely have *more* stuff to store in there,
-- so, don't make that too low, period ;).
-- NOTE: Revisit when we bump MµPDF by doing a few tests with the tracing memory allocators,
-- as even 32MB is likely to be too conservative.
local mupdf = {
debug_memory = false,
cache_size = 32*1024*1024,
}
-- this cannot get adapted by the cdecl file because it is a
-- string constant. Must match the actual mupdf API:
local FZ_VERSION = "1.26.10"
local document_mt = { __index = {} }
local page_mt = { __index = {} }
mupdf.debug = function() --[[ no debugging by default ]] end
local function drop_context(ctx)
local refcount = ffi.cast("int *", M.fz_user_context(ctx))
refcount[0] = refcount[0] - 1
if refcount[0] == 0 then
M.fz_drop_context(ctx)
C.free(refcount)
end
end
local function keep_context(ctx)
local refcount = ffi.cast("int *", M.fz_user_context(ctx))
refcount[0] = refcount[0] + 1
return ctx
end
local save_ctx = setmetatable({}, {__mode="kv"})
-- provides an fz_context for mupdf
local function context()
local ctx = save_ctx[1]
if ctx then return ctx end
ctx = M.fz_new_context_imp(
mupdf.debug_memory and W.mupdf_get_my_alloc_context() or nil,
nil,
mupdf.cache_size, FZ_VERSION)
if ctx == nil then
error("cannot create fz_context for MuPDF")
end
local refcount = ffi.cast("int *", C.malloc(ffi.sizeof("int")))
M.fz_set_user_context(ctx, refcount)
refcount[0] = 1
-- ctx is a cdata<fz_context *>, attach a finalizer to it to release ressources on garbage collection
ctx = ffi.gc(ctx, drop_context)
M.fz_install_external_font_funcs(ctx)
M.fz_register_document_handlers(ctx)
save_ctx[1] = ctx
return ctx
end
-- a wrapper for mupdf exception error messages
local function merror(ctx, message)
error(string.format("%s: %s (%d)", message,
ffi.string(W.mupdf_error_message(ctx)),
W.mupdf_error_code(ctx)))
end
local function drop_document(ctx, doc)
-- Clear the cdata finalizer to avoid a double-free
ffi.gc(doc, nil)
M.fz_drop_document(ctx, doc)
drop_context(ctx)
end
local function drop_page(ctx, page)
-- Clear the cdata finalizer to avoid a double-free
ffi.gc(page, nil)
M.fz_drop_page(ctx, page)
drop_context(ctx)
end
--[[--
Opens a document.
--]]
function mupdf.openDocument(filename)
local ctx = context()
local mupdf_doc = {
doc = W.mupdf_open_document(ctx, filename),
filename = filename,
}
if mupdf_doc.doc == nil then
merror(ctx, "MuPDF cannot open file.")
end
-- doc is a cdata<fz_document *>, attach a finalizer to it to release ressources on garbage collection
mupdf_doc.doc = ffi.gc(mupdf_doc.doc, function(doc) drop_document(ctx, doc) end)
mupdf_doc.ctx = keep_context(ctx)
setmetatable(mupdf_doc, document_mt)
if mupdf_doc:getPages() <= 0 then
merror(ctx, "MuPDF found no pages in file.")
end
return mupdf_doc
end
function mupdf.openDocumentFromText(text, magic, html_resource_directory)
local ctx = context()
local stream = W.mupdf_open_memory(ctx, ffi.cast("const unsigned char*", text), #text)
local archive = nil
if html_resource_directory ~= nil then
archive = W.mupdf_open_directory(ctx, html_resource_directory)
end
local mupdf_doc = {
doc = W.mupdf_open_document_with_stream_and_dir(ctx, magic, stream, archive),
}
W.mupdf_drop_stream(ctx, stream)
if archive ~= nil then
W.mupdf_drop_archive(ctx, archive)
end
if mupdf_doc.doc == nil then
merror(ctx, "MuPDF cannot open document from text")
end
-- doc is a cdata<fz_document *>, attach a finalizer to it to release ressources on garbage collection
mupdf_doc.doc = ffi.gc(mupdf_doc.doc, function(doc) drop_document(ctx, doc) end)
mupdf_doc.ctx = keep_context(ctx)
setmetatable(mupdf_doc, document_mt)
return mupdf_doc
end
-- Document functions:
--[[
close the document
this is done automatically by the garbage collector but can be
triggered explicitly
--]]
function document_mt.__index:close()
if self.doc ~= nil then
drop_document(self.ctx, self.doc)
self.doc = nil
self.ctx = nil
end
end
--[[
check if the document needs a password for access
--]]
function document_mt.__index:needsPassword()
return M.fz_needs_password(self.ctx, self.doc) ~= 0
end
--[[
try to authenticate with a password
--]]
function document_mt.__index:authenticatePassword(password)
if M.fz_authenticate_password(self.ctx, self.doc, password) == 0 then
return false
end
return true
end
--[[
read number of pages in document
--]]
function document_mt.__index:getPages()
-- cache number of pages
if self.number_of_pages then return self.number_of_pages end
local pages = W.mupdf_count_pages(self.ctx, self.doc)
if pages == -1 then
merror(self.ctx, "cannot access page tree")
end
self.number_of_pages = pages
return pages
end
function document_mt.__index:isDocumentReflowable()
if self.is_reflowable then return self.is_reflowable end
self.is_reflowable = M.fz_is_document_reflowable(self.ctx, self.doc) == 1
return self.is_reflowable
end
function document_mt.__index:layoutDocument(width, height, em)
-- Reset the cache.
self.number_of_pages = nil
W.mupdf_layout_document(self.ctx, self.doc, width, height, em)
end
function document_mt.__index:setColorRendering(color)
self.color = color
end
local function toc_walker(toc, outline, depth)
while outline ~= nil do
table.insert(toc, {
page = outline.page.page + 1,
title = ffi.string(outline.title),
depth = depth,
})
if outline.down then
toc_walker(toc, outline.down, depth+1)
end
outline = outline.next
end
end
--[[
read table of contents (ToC)
Returns a table like this:
{
{page=12, depth=1, title="chapter1"},
{page=54, depth=1, title="chapter2"},
}
Returns an empty table when there is no ToC
--]]
function document_mt.__index:getToc()
local toc = {}
local outline = W.mupdf_load_outline(self.ctx, self.doc)
if outline ~= nil then
toc_walker(toc, outline, 1)
M.fz_drop_outline(self.ctx, outline)
end
return toc
end
--[[
open a page, return page object
--]]
function document_mt.__index:openPage(number)
local ctx = self.ctx
local mupdf_page = {
page = W.mupdf_load_page(ctx, self.doc, number-1),
number = number,
doc = self,
}
if mupdf_page.page == nil then
merror(ctx, "cannot open page #" .. number)
end
-- page is a cdata<fz_page *>, attach a finalizer to it to release ressources on garbage collection
mupdf_page.page = ffi.gc(mupdf_page.page, function(page) drop_page(ctx, page) end)
mupdf_page.ctx = keep_context(ctx)
setmetatable(mupdf_page, page_mt)
return mupdf_page
end
local function getMetadataInfo(ctx, doc, info)
local bufsize = 255
local buf = ffi.new("char[?]", bufsize)
-- `fz_lookup_metadata` return the number of bytes needed
-- to store the string, **including** the null terminator.
local res = M.fz_lookup_metadata(ctx, doc, info, buf, bufsize)
if res > bufsize then
-- Buffer was too small.
bufsize = res
buf = ffi.new("char[?]", bufsize)
res = M.fz_lookup_metadata(ctx, doc, info, buf, bufsize)
end
if res > 1 then
-- Note: strip the null terminator.
return ffi.string(buf, res - 1)
end
-- Empty string or error (-1).
return ""
end
--[[
Get metadata, return object
--]]
function document_mt.__index:getMetadata()
local metadata = {
title = getMetadataInfo(self.ctx, self.doc, "info:Title"),
author = getMetadataInfo(self.ctx, self.doc, "info:Author"),
subject = getMetadataInfo(self.ctx, self.doc, "info:Subject"),
keywords = getMetadataInfo(self.ctx, self.doc, "info:Keywords"),
creator = getMetadataInfo(self.ctx, self.doc, "info:Creator"),
producer = getMetadataInfo(self.ctx, self.doc, "info:Producer"),
creationDate = getMetadataInfo(self.ctx, self.doc, "info:CreationDate"),
modDate = getMetadataInfo(self.ctx, self.doc, "info:ModDate")
}
return metadata
end
--[[
return currently claimed memory by MuPDF
This will return sensible values only when the debug_memory flag is set
--]]
function document_mt.__index:getCacheSize()
if mupdf.debug_memory then
return W.mupdf_get_cache_size()
else
return 0
end
end
function document_mt.__index:cleanCache()
-- NOP, just for API compatibility
end
--[[
write the document to a new file
--]]
function document_mt.__index:writeDocument(filename)
local opts = ffi.new("pdf_write_options[1]")
opts[0].do_incremental = (filename == self.filename ) and 1 or 0
opts[0].do_ascii = 0
opts[0].do_garbage = 0
opts[0].do_linear = 0
local ok = W.mupdf_pdf_save_document(self.ctx, ffi.cast("pdf_document*", self.doc), filename, opts)
if ok == nil then merror(self.ctx, "could not write document") end
end
-- Page functions:
--[[
explicitly close the page object
this is done implicitly by garbage collection, too.
--]]
function page_mt.__index:close()
if self.page ~= nil then
drop_page(self.ctx, self.page)
self.page = nil
self.ctx = nil
end
end
--[[
calculate page size after applying DrawContext
--]]
function page_mt.__index:getSize(draw_context)
local bounds = ffi.new("fz_rect")
local ctm = ffi.new("fz_matrix")
local zx, zy
if type(draw_context.zoom) == "table" then
zx = draw_context.zoom.x
zy = draw_context.zoom.y
else
zx = draw_context.zoom
zy = draw_context.zoom
end
W.mupdf_fz_scale(ctm, zx, zy)
W.mupdf_fz_pre_rotate(ctm, draw_context.rotate)
W.mupdf_fz_bound_page(self.ctx, self.page, bounds)
W.mupdf_fz_transform_rect(bounds, ctm)
-- NOTE: fz_bound_page returns an internal representation computed @ 72dpi...
-- It is often superbly mysterious, even for images,
-- so we do *NOT* want to round it right now,
-- as it would introduce rounding errors much too early in the pipeline...
-- NOTE: ReaderZooming uses it to compute the scale factor, where accuracy matters!
-- NOTE: This is also used in conjunction with getUsedBBox,
-- which also returns precise, floating point rectangles!
--[[
M.fz_round_rect(bbox, bounds)
return bbox[0].x1-bbox[0].x0, bbox[0].y1-bbox[0].y0
--]]
return bounds.x1 - bounds.x0, bounds.y1 - bounds.y0
end
--[[
check which part of the page actually contains content
--]]
function page_mt.__index:getUsedBBox()
local result = ffi.new("fz_rect")
local dev = W.mupdf_new_bbox_device(self.ctx, result)
if dev == nil then merror(self.ctx, "cannot allocate bbox_device") end
local ok = W.mupdf_run_page(self.ctx, self.page, dev, M.fz_identity, nil)
M.fz_close_device(self.ctx, dev)
M.fz_drop_device(self.ctx, dev)
if ok == nil then merror(self.ctx, "cannot calculate bbox for page") end
return result.x0, result.y0, result.x1, result.y1
end
local B = string.byte
local function is_unicode_wspace(c)
return c == 9 or -- TAB
c == 0x0a or -- HT
c == 0x0b or -- LF
c == 0x0c or -- VT
c == 0x0d or -- FF
c == 0x20 or -- CR
c == 0x85 or -- NEL
c == 0xA0 or -- No break space
c == 0x1680 or -- Ogham space mark
c == 0x180E or -- Mongolian Vowel Separator
c == 0x2000 or -- En quad
c == 0x2001 or -- Em quad
c == 0x2002 or -- En space
c == 0x2003 or -- Em space
c == 0x2004 or -- Three-per-Em space
c == 0x2005 or -- Four-per-Em space
c == 0x2006 or -- Five-per-Em space
c == 0x2007 or -- Figure space
c == 0x2008 or -- Punctuation space
c == 0x2009 or -- Thin space
c == 0x200A or -- Hair space
c == 0x2028 or -- Line separator
c == 0x2029 or -- Paragraph separator
c == 0x202F or -- Narrow no-break space
c == 0x205F or -- Medium mathematical space
c == 0x3000 -- Ideographic space
end
local function is_unicode_bullet(c)
-- Not all of them are strictly bullets, but will do for our usage here
return c == 0x2022 or -- Bullet
c == 0x2023 or -- Triangular bullet
c == 0x25a0 or -- Black square
c == 0x25cb or -- White circle
c == 0x25cf or -- Black circle
c == 0x25e6 or -- White bullet
c == 0x2043 or -- Hyphen bullet
c == 0x2219 or -- Bullet operator
c == 149 or -- Ascii bullet
c == B'*'
end
local function skip_starting_bullet(line)
local ch = line.first_char
local found_bullet = false
while ch ~= nil do
if is_unicode_bullet(ch.c) then
found_bullet = true
elseif not is_unicode_wspace(ch.c) then
break
end
ch = ch.next
end
if found_bullet then
return ch
else
return line.first_char
end
end
--[[
get the text of the given page
will return text in a Lua table that is modeled after
djvu.c creates this table.
note that the definition of "line" is somewhat arbitrary
here (for now)
MuPDFs API provides text as single char information
that is collected in "spans". we use a span as a "line"
in Lua output and segment spans into words by looking
for space characters.
will return an empty table if we have no text
--]]
function page_mt.__index:getPageText()
-- first, we run the page through a special device, the text_device
local text_page = W.mupdf_new_stext_page_from_page(self.ctx, self.page, nil)
if text_page == nil then merror(self.ctx, "cannot alloc text_page") end
-- now we analyze the data returned by the device and bring it
-- into the format we want to return
local lines = {}
local size = 0
local block = text_page.first_block
while block ~= nil do
if block.type == M.FZ_STEXT_BLOCK_TEXT then
-- a block contains lines, which is our primary return datum
local mupdf_line = block.u.t.first_line
while mupdf_line ~= nil do
local line = {}
local line_bbox = ffi.new("fz_rect", M.fz_empty_rect)
local first_char = skip_starting_bullet( mupdf_line )
local ch = first_char
local ch_len = 0
while ch ~= nil do
ch = ch.next
ch_len = ch_len + 1
end
if ch_len > 0 then
-- here we will collect UTF-8 chars before making them
-- a Lua string:
local textbuf = ffi.new("char[?]", ch_len * 4)
ch = first_char
while ch ~= nil do
local textlen = 0
local word_bbox = ffi.new("fz_rect", M.fz_empty_rect)
while ch ~= nil do
if is_unicode_wspace(ch.c) then
-- ignore and end word
break
end
textlen = textlen + M.fz_runetochar(textbuf + textlen, ch.c)
local char_bbox = ffi.new("fz_rect")
W.mupdf_fz_rect_from_quad(char_bbox, ch.quad)
W.mupdf_fz_union_rect(word_bbox, char_bbox)
W.mupdf_fz_union_rect(line_bbox, char_bbox)
if ch.c >= 0x4e00 and ch.c <= 0x9FFF or -- CJK Unified Ideographs
ch.c >= 0x2000 and ch.c <= 0x206F or -- General Punctuation
ch.c >= 0x3000 and ch.c <= 0x303F or -- CJK Symbols and Punctuation
ch.c >= 0x3400 and ch.c <= 0x4DBF or -- CJK Unified Ideographs Extension A
ch.c >= 0xF900 and ch.c <= 0xFAFF or -- CJK Compatibility Ideographs
ch.c >= 0xFF01 and ch.c <= 0xFFEE or -- Halfwidth and Fullwidth Forms
ch.c >= 0x20000 and ch.c <= 0x2A6DF -- CJK Unified Ideographs Extension B
then
-- end word
break
end
ch = ch.next
end
-- add word to line
if word_bbox.x0 < word_bbox.x1 and word_bbox.y0 < word_bbox.y1 then
table.insert(line, {
word = ffi.string(textbuf, textlen),
x0 = word_bbox.x0, y0 = word_bbox.y0,
x1 = word_bbox.x1, y1 = word_bbox.y1,
})
size = size + 5 * 8 + textlen
end
if ch == nil then
break
end
ch = ch.next
end
if line_bbox.x0 < line_bbox.x1 and line_bbox.y0 < line_bbox.y1 then
line.x0, line.y0 = line_bbox.x0, line_bbox.y0
line.x1, line.y1 = line_bbox.x1, line_bbox.y1
size = size + 5 * 8
table.insert(lines, line)
end
end
mupdf_line = mupdf_line.next
end
end
block = block.next
end
-- Rough approximation of size for caching
lines.size = size
M.fz_drop_stext_page(self.ctx, text_page)
return lines
end
--[[
Get a list of the Hyperlinks on a page
--]]
function page_mt.__index:getPageLinks()
local page_links = W.mupdf_load_links(self.ctx, self.page)
-- do not error out when page_links == NULL, since there might
-- simply be no links present.
local links = {}
local link = page_links
while link ~= nil do
local data = {
x0 = link.rect.x0, y0 = link.rect.y0,
x1 = link.rect.x1, y1 = link.rect.y1,
}
local pos = ffi.new("float[2]")
local location = ffi.new("fz_location")
W.mupdf_fz_resolve_link(self.ctx, self.doc.doc, link.uri, pos, pos+1, location)
-- `fz_resolve_link` return a location of (-1, -1) for external links.
if location.chapter == -1 and location.page == -1 then
data.uri = ffi.string(link.uri)
else
data.page = W.mupdf_fz_page_number_from_location(self.ctx, self.doc.doc, location)
end
data.pos = {
x = pos[0], y = pos[1],
}
table.insert(links, data)
link = link.next
end
M.fz_drop_link(self.ctx, page_links)
return links
end
local function run_page(page, pixmap, ctm)
M.fz_clear_pixmap_with_value(page.ctx, pixmap, 0xff)
local dev = W.mupdf_new_draw_device(page.ctx, nil, pixmap)
if dev == nil then merror(page.ctx, "cannot create draw device") end
local ok = W.mupdf_run_page(page.ctx, page.page, dev, ctm, nil)
M.fz_close_device(page.ctx, dev)
M.fz_drop_device(page.ctx, dev)
if ok == nil then merror(page.ctx, "could not run page") end
end
--[[
render page to blitbuffer
old interface: expects a blitbuffer to render to
--]]
function page_mt.__index:draw(draw_context, blitbuffer, offset_x, offset_y)
local buffer = self:draw_new(draw_context, blitbuffer:getWidth(), blitbuffer:getHeight(), offset_x, offset_y)
blitbuffer:blitFrom(buffer)
buffer:free()
end
--[[
render page to blitbuffer
new interface: creates the blitbuffer with the rendered data and returns that
TODO: make this the used interface
--]]
function page_mt.__index:draw_new(draw_context, width, height, offset_x, offset_y)
local ctm = ffi.new("fz_matrix")
-- HACK GEMINI V8: SMART STRETCH (Fit Width ONLY)
local forced = false
if Screen then
local sw = Screen:getWidth()
local sh = Screen:getHeight()
-- On detecte l'orientation
local screen_target_w = sw
local screen_target_h = sh
if draw_context.rotate == 90 or draw_context.rotate == 270 then
screen_target_w = sh
screen_target_h = sw
end
-- CONDITION SMART : Active le stretch uniquement si la largeur demandee = largeur ecran
local is_fit_width = math.abs(width - screen_target_w) < 5
if is_fit_width then
local bounds = ffi.new("fz_rect")
W.mupdf_fz_bound_page(self.ctx, self.page, bounds)
local pw = bounds.x1 - bounds.x0
local ph = bounds.y1 - bounds.y0
if pw > 0 and ph > 0 then
width = screen_target_w
height = screen_target_h
ctm.a=1; ctm.b=0; ctm.c=0; ctm.d=1; ctm.e=0; ctm.f=0
local sx = width / pw
local sy = height / ph
W.mupdf_fz_scale(ctm, sx, sy)
if draw_context.rotate ~= 0 then
W.mupdf_fz_pre_rotate(ctm, draw_context.rotate)
end
forced = true
end
end
end
if not forced then
W.mupdf_fz_scale(ctm, draw_context.zoom, draw_context.zoom)
W.mupdf_fz_pre_rotate(ctm, draw_context.rotate)
W.mupdf_fz_pre_translate(ctm, draw_context.offset_x, draw_context.offset_y)
end
local bbox = ffi.new("fz_irect", offset_x, offset_y, offset_x + width, offset_y + height)
local bb = BlitBuffer.new(width, height, self.doc.color and BlitBuffer.TYPE_BBRGB32 or BlitBuffer.TYPE_BB8)
local colorspace = self.doc.color and M.fz_device_rgb(self.ctx)
or M.fz_device_gray(self.ctx)
if mupdf.bgr and self.doc.color then
colorspace = M.fz_device_bgr(self.ctx)
end
local pix = W.mupdf_new_pixmap_with_bbox_and_data(
self.ctx, colorspace, bbox, nil, self.doc.color and 1 or 0, ffi.cast("unsigned char*", bb.data))
if pix == nil then merror(self.ctx, "cannot allocate pixmap") end
run_page(self, pix, ctm)
if draw_context.gamma >= 0.0 then
M.fz_gamma_pixmap(self.ctx, pix, draw_context.gamma)
end
M.fz_drop_pixmap(self.ctx, pix)
return bb
end
mupdf.STRIKE_HEIGHT = 0.375
mupdf.UNDERLINE_HEIGHT = 0
mupdf.LINE_THICKNESS = 0.05
mupdf.HIGHLIGHT_COLOR = {1.0, 1.0, 0.0}
mupdf.UNDERLINE_COLOR = {0.0, 0.0, 1.0}
mupdf.STRIKE_OUT_COLOR = {1.0, 0.0, 0.0}
function page_mt.__index:addMarkupAnnotation(points, n, type, bb_color)
local color = ffi.new("float[3]")
local alpha = 1.0
if type == M.PDF_ANNOT_HIGHLIGHT then
if bb_color then
color[0] = bb_color.r / 255
color[1] = bb_color.g / 255
color[2] = bb_color.b / 255
else
color[0] = mupdf.HIGHLIGHT_COLOR[1]
color[1] = mupdf.HIGHLIGHT_COLOR[2]
color[2] = mupdf.HIGHLIGHT_COLOR[3]
end
alpha = 0.5
elseif type == M.PDF_ANNOT_UNDERLINE then
if bb_color then
color[0] = bb_color.r / 255
color[1] = bb_color.g / 255
color[2] = bb_color.b / 255
else
color[0] = mupdf.UNDERLINE_COLOR[1]
color[1] = mupdf.UNDERLINE_COLOR[2]
color[2] = mupdf.UNDERLINE_COLOR[3]
end
elseif type == M.PDF_ANNOT_STRIKE_OUT then
if bb_color then
color[0] = bb_color.r / 255
color[1] = bb_color.g / 255
color[2] = bb_color.b / 255
else
color[0] = mupdf.STRIKE_OUT_COLOR[1]
color[1] = mupdf.STRIKE_OUT_COLOR[2]
color[2] = mupdf.STRIKE_OUT_COLOR[3]
end
else
return
end
local annot = W.mupdf_pdf_create_annot(self.ctx, ffi.cast("pdf_page*", self.page), type)
if annot == nil then merror(self.ctx, "could not create annotation") end
local ok = W.mupdf_pdf_set_annot_quad_points(self.ctx, annot, n, points)
if ok == nil then merror(self.ctx, "could not set annotation quadpoints") end
ok = W.mupdf_pdf_set_annot_color(self.ctx, annot, 3, color)
if ok == nil then merror(self.ctx, "could not set annotation color") end
ok = W.mupdf_pdf_set_annot_opacity(self.ctx, annot, alpha)
if ok == nil then merror(self.ctx, "could not set annotation opacity") end
-- Fetch back MuPDF's stored coordinates of all quadpoints, as they may have been modified/rounded
-- (we need the exact ones that were saved if we want to be able to find them for deletion/update)
for i = 0, n-1 do
W.mupdf_pdf_annot_quad_point(self.ctx, annot, i, points+i)
end
end
function page_mt.__index:deleteMarkupAnnotation(annot)
local ok = W.mupdf_pdf_delete_annot(self.ctx, ffi.cast("pdf_page*", self.page), annot)
if ok == nil then merror(self.ctx, "could not delete markup annotation") end
end
function page_mt.__index:getMarkupAnnotation(points, n)
local annot = W.mupdf_pdf_first_annot(self.ctx, ffi.cast("pdf_page*", self.page))
while annot ~= nil do
local n2 = W.mupdf_pdf_annot_quad_point_count(self.ctx, annot)
if n == n2 then
local quadpoint = ffi.new("fz_quad[1]")
local match = true
for i = 0, n-1 do
W.mupdf_pdf_annot_quad_point(self.ctx, annot, i, quadpoint)
if (points[i].ul.x ~= quadpoint[0].ul.x or
points[i].ul.y ~= quadpoint[0].ul.y or
points[i].ur.x ~= quadpoint[0].ur.x or
points[i].ur.y ~= quadpoint[0].ur.y or
points[i].ll.x ~= quadpoint[0].ll.x or
points[i].ll.y ~= quadpoint[0].ll.y or
points[i].lr.x ~= quadpoint[0].lr.x or
points[i].lr.y ~= quadpoint[0].lr.y) then
match = false
break
end
end
if match then return annot end
end
annot = W.mupdf_pdf_next_annot(self.ctx, annot)
end
return nil
end
function page_mt.__index:updateMarkupAnnotation(annot, contents)
local ok = W.mupdf_pdf_set_annot_contents(self.ctx, annot, contents)
if ok == nil then merror(self.ctx, "could not update markup annot contents") end
end
-- image loading via MuPDF:
--[[--
Renders image data.
--]]
function mupdf.renderImage(data, size, width, height)
local ctx = context()
local buffer = W.mupdf_new_buffer_from_shared_data(ctx,
ffi.cast("unsigned char*", data), size)
local image = W.mupdf_new_image_from_buffer(ctx, buffer)
W.mupdf_drop_buffer(ctx, buffer)
if image == nil then merror(ctx, "could not load image data") end
local pixmap = W.mupdf_get_pixmap_from_image(ctx,
image, nil, nil, nil, nil)
M.fz_drop_image(ctx, image)
if pixmap == nil then
merror(ctx, "could not create pixmap from image")
end
local p_width = M.fz_pixmap_width(ctx, pixmap)
local p_height = M.fz_pixmap_height(ctx, pixmap)
-- mupdf_get_pixmap_from_image() may not scale image to the
-- width and height provided, so check and scale it if needed
if width and height then
-- Ensure we pass integer values for width & height to fz_scale_pixmap(),
-- because it enforces an alpha channel otherwise...
width = math.floor(width)
height = math.floor(height)
if p_width ~= width or p_height ~= height then
local scaled_pixmap = M.fz_scale_pixmap(ctx, pixmap, 0, 0, width, height, nil)
M.fz_drop_pixmap(ctx, pixmap)
if scaled_pixmap == nil then
merror(ctx, "could not create scaled pixmap from pixmap")
end
pixmap = scaled_pixmap
p_width = M.fz_pixmap_width(ctx, pixmap)
p_height = M.fz_pixmap_height(ctx, pixmap)
end
end
local bbtype
local ncomp = M.fz_pixmap_components(ctx, pixmap)
if ncomp == 1 then bbtype = BlitBuffer.TYPE_BB8
elseif ncomp == 2 then bbtype = BlitBuffer.TYPE_BB8A
elseif ncomp == 3 then bbtype = BlitBuffer.TYPE_BBRGB24
elseif ncomp == 4 then bbtype = BlitBuffer.TYPE_BBRGB32
else error("unsupported number of color components")
end
-- Handle RGB->BGR conversion for Kobos when needed
local bb
if mupdf.bgr and ncomp >= 3 then
local bgr_pixmap = W.mupdf_convert_pixmap(ctx, pixmap, M.fz_device_bgr(ctx), nil, nil, M.fz_default_color_params, (ncomp == 4 and 1 or 0))
if pixmap == nil then
merror(ctx, "could not convert pixmap to BGR")
end
M.fz_drop_pixmap(ctx, pixmap)
local p = M.fz_pixmap_samples(ctx, bgr_pixmap)
bb = BlitBuffer.new(p_width, p_height, bbtype, p):copy()
M.fz_drop_pixmap(ctx, bgr_pixmap)
else
local p = M.fz_pixmap_samples(ctx, pixmap)
bb = BlitBuffer.new(p_width, p_height, bbtype, p):copy()
M.fz_drop_pixmap(ctx, pixmap)
end
return bb
end
--- Renders image file.
function mupdf.renderImageFile(filename, width, height)
local file = io.open(filename, "rb")
if not file then error("could not open image file") end
local data = file:read("*a")
file:close()
return mupdf.renderImage(data, #data, width, height)
end
--[[--
Scales a blitbuffer.
MµPDF's scaling is of much better quality than the very naive implementation in blitbuffer.lua.
(see fz_scale_pixmap_cached() in mupdf/source/fitz/draw-scale-simple.c).
Same arguments as BlitBuffer:scale() for easy replacement.
Unlike BlitBuffer:scale(), this *ignores* the blitbuffer's rotation
(i.e., where possible, we simply wrap the BlitBuffer's data in a fitz pixmap,
with no data copy, so the buffer's *native* memory layout is followed).
If you actually want to preserve the rotation, you'll have to fudge
with the width & height arguments and tweak the returned buffer's rotation flag,
or go through a temporary copy to ensure that the buffer's memory is laid out accordingly.
--]]
function mupdf.scaleBlitBuffer(bb, width, height)
-- We need first to convert our BlitBuffer to a pixmap
local bbtype = bb:getType()
local colorspace
local converted_bb
local alpha
local stride = bb.stride
local ctx = context()
-- MuPDF should know how to handle *most* of our BB types,
-- special snowflakes excluded (4bpp & RGB565),
-- in which case we feed it a temporary copy in the closest format it'll understand.
if bbtype == BlitBuffer.TYPE_BB8 then
colorspace = M.fz_device_gray(ctx)
alpha = 0
elseif bbtype == BlitBuffer.TYPE_BB8A then
colorspace = M.fz_device_gray(ctx)
alpha = 1
elseif bbtype == BlitBuffer.TYPE_BBRGB24 then
if mupdf.bgr then
colorspace = M.fz_device_bgr(ctx)
else
colorspace = M.fz_device_rgb(ctx)
end
alpha = 0
elseif bbtype == BlitBuffer.TYPE_BBRGB32 then
if mupdf.bgr then
colorspace = M.fz_device_bgr(ctx)
else
colorspace = M.fz_device_rgb(ctx)
end
alpha = 1
elseif bbtype == BlitBuffer.TYPE_BB4 then
converted_bb = BlitBuffer.new(bb.w, bb.h, BlitBuffer.TYPE_BB8)
converted_bb:blitFrom(bb, 0, 0, 0, 0, bb.w, bb.h)
bb = converted_bb -- we don't free() the provided bb, but we'll have to free our converted_bb
colorspace = M.fz_device_gray(ctx)
alpha = 0
stride = bb.w
else
converted_bb = BlitBuffer.new(bb.w, bb.h, BlitBuffer.TYPE_BBRGB32)
converted_bb:blitFrom(bb, 0, 0, 0, 0, bb.w, bb.h)
bb = converted_bb -- we don't free() the provided bb, but we'll have to free our converted_bb
if mupdf.bgr then
colorspace = M.fz_device_bgr(ctx)
else
colorspace = M.fz_device_rgb(ctx)
end
alpha = 1
end
-- We can now create a pixmap from this bb of correct type
local pixmap = W.mupdf_new_pixmap_with_data(ctx, colorspace,
bb.w, bb.h, nil, alpha, stride, ffi.cast("unsigned char*", bb.data))
if pixmap == nil then
if converted_bb then converted_bb:free() end -- free our home made bb
merror(ctx, "could not create pixmap from blitbuffer")
end
-- We can now scale the pixmap
-- Better to ensure we give integer width and height, to avoid a black 1-pixel line at right and bottom of image.
-- Also, fz_scale_pixmap enforces an alpha channel if w or h are floats...
local scaled_pixmap = M.fz_scale_pixmap(ctx, pixmap, 0, 0, math.floor(width), math.floor(height), nil)
M.fz_drop_pixmap(ctx, pixmap) -- free our original pixmap
if scaled_pixmap == nil then
if converted_bb then converted_bb:free() end -- free our home made bb
merror(ctx, "could not create scaled pixmap from pixmap")
end
local p_width = M.fz_pixmap_width(ctx, scaled_pixmap)
local p_height = M.fz_pixmap_height(ctx, scaled_pixmap)
-- And convert the pixmap back to a BlitBuffer
bbtype = nil
local ncomp = M.fz_pixmap_components(ctx, scaled_pixmap)
if ncomp == 1 then bbtype = BlitBuffer.TYPE_BB8
elseif ncomp == 2 then bbtype = BlitBuffer.TYPE_BB8A
elseif ncomp == 3 then bbtype = BlitBuffer.TYPE_BBRGB24
elseif ncomp == 4 then bbtype = BlitBuffer.TYPE_BBRGB32
else
if converted_bb then converted_bb:free() end -- free our home made bb
error("unsupported number of color components")
end
local p = M.fz_pixmap_samples(ctx, scaled_pixmap)
bb = BlitBuffer.new(p_width, p_height, bbtype, p):copy()
M.fz_drop_pixmap(ctx, scaled_pixmap) -- free our scaled pixmap
if converted_bb then converted_bb:free() end -- free our home made bb
return bb
end
-- k2pdfopt interfacing
-- will lazily load ffi/koptcontext.lua in order to interface k2pdfopt
local cached_k2pdfopt
local function get_k2pdfopt()
if cached_k2pdfopt then return cached_k2pdfopt end
local koptcontext = require("ffi/koptcontext")
cached_k2pdfopt = koptcontext.k2pdfopt
return cached_k2pdfopt
end
--[[
the following function is a reimplementation of what can be found
in libk2pdfopt/willuslib/bmpmupdf.c
k2pdfopt supports only 8bit and 24bit "bitmaps" - and mupdf will give
only 8bit+8bit alpha or 24bit+8bit alpha pixmaps. So we need to convert
what we get from mupdf.
--]]
local function bmpmupdf_pixmap_to_bmp(bmp, pixmap)
local k2pdfopt = get_k2pdfopt()
local ctx = context()
bmp.width = M.fz_pixmap_width(ctx, pixmap)
bmp.height = M.fz_pixmap_height(ctx, pixmap)
local ncomp = M.fz_pixmap_components(ctx, pixmap)
local p = M.fz_pixmap_samples(ctx, pixmap)
if ncomp == 2 or ncomp == 4 then
k2pdfopt.pixmap_to_bmp(bmp, p, ncomp)
else
error("unsupported pixmap format for conversion to bmp")
end
end
local function render_for_kopt(bmp, page, scale, bounds)
local k2pdfopt = get_k2pdfopt()
local bbox = ffi.new("fz_irect")
local ctm = ffi.new("fz_matrix")
W.mupdf_fz_scale(ctm, scale, scale)
W.mupdf_fz_transform_rect(bounds, ctm)
W.mupdf_fz_round_rect(bbox, bounds)
local colorspace = page.doc.color and M.fz_device_rgb(page.ctx)
or M.fz_device_gray(page.ctx)
if mupdf.bgr and page.doc.color then
colorspace = M.fz_device_bgr(page.ctx)
end
local pix = W.mupdf_new_pixmap_with_bbox(page.ctx, colorspace, bbox, nil, 1)
if pix == nil then merror(page.ctx, "could not allocate pixmap") end
run_page(page, pix, ctm)
k2pdfopt.bmp_init(bmp)
bmpmupdf_pixmap_to_bmp(bmp, pix)
M.fz_drop_pixmap(page.ctx, pix)
end
function page_mt.__index:getPagePix(kopt_context)
local bounds = ffi.new("fz_rect", kopt_context.bbox.x0, kopt_context.bbox.y0, kopt_context.bbox.x1, kopt_context.bbox.y1)
render_for_kopt(kopt_context.src, self, kopt_context.zoom, bounds)
kopt_context.page_width = kopt_context.src.width
kopt_context.page_height = kopt_context.src.height
end
function page_mt.__index:toBmp(bmp, dpi)
local bounds = ffi.new("fz_rect")
W.mupdf_fz_bound_page(self.ctx, self.page, bounds)
render_for_kopt(bmp, self, dpi/72, bounds)
end
return mupdf
readerpaging.lua
local BD = require("ui/bidi")
local Device = require("device")
local Event = require("ui/event")
local Geom = require("ui/geometry")
local InputContainer = require("ui/widget/container/inputcontainer")
local Math = require("optmath")
local UIManager = require("ui/uimanager")
local bit = require("bit")
local logger = require("logger")
local util = require("util")
local time = require("ui/time")
local _ = require("gettext")
local Input = Device.input
local Screen = Device.screen
local function copyPageState(page_state)
return {
page = page_state.page,
zoom = page_state.zoom,
rotation = page_state.rotation,
gamma = page_state.gamma,
offset = page_state.offset:copy(),
visible_area = page_state.visible_area:copy(),
page_area = page_state.page_area:copy(),
}
end
local ReaderPaging = InputContainer:extend{
pan_rate = 30, -- default 30 ops, will be adjusted in readerui
current_page = 0,
number_of_pages = 0,
visible_area = nil,
page_area = nil,
overlap = Screen:scaleBySize(G_defaults:readSetting("DOVERLAPPIXELS")),
page_flipping_mode = false,
bookmark_flipping_mode = false,
flip_steps = {0, 1, 2, 5, 10, 20, 50, 100},
}
function ReaderPaging:init()
self:registerKeyEvents()
self.pan_interval = time.s(1 / self.pan_rate)
self.number_of_pages = self.ui.document.info.number_of_pages
-- delegate gesture listener to readerui, NOP our own
self.ges_events = nil
end
function ReaderPaging:onGesture() end
function ReaderPaging:registerKeyEvents()
local nextKey = BD.mirroredUILayout() and "Left" or "Right"
local prevKey = BD.mirroredUILayout() and "Right" or "Left"
if Device:hasDPad() and Device:useDPadAsActionKeys() then
if G_reader_settings:isTrue("left_right_keys_turn_pages") then
self.key_events.GotoNextPage = { { { "RPgFwd", "LPgFwd", nextKey, " " } }, event = "GotoViewRel", args = 1, }
self.key_events.GotoPrevPage = { { { "RPgBack", "LPgBack", prevKey } }, event = "GotoViewRel", args = -1, }
elseif G_reader_settings:nilOrFalse("left_right_keys_turn_pages") then
self.key_events.GotoNextChapter = { { nextKey }, event = "GotoNextChapter", args = 1, }
self.key_events.GotoPrevChapter = { { prevKey }, event = "GotoPrevChapter", args = -1, }
self.key_events.GotoNextPage = { { { "RPgFwd", "LPgFwd", " " } }, event = "GotoViewRel", args = 1, }
self.key_events.GotoPrevPage = { { { "RPgBack", "LPgBack" } }, event = "GotoViewRel", args = -1, }
end
elseif Device:hasKeys() then
self.key_events.GotoNextPage = { { { "RPgFwd", "LPgFwd", not Device:hasFewKeys() and nextKey } }, event = "GotoViewRel", args = 1, }
self.key_events.GotoPrevPage = { { { "RPgBack", "LPgBack", not Device:hasFewKeys() and prevKey } }, event = "GotoViewRel", args = -1, }
self.key_events.GotoNextPos = { { "Down" }, event = "GotoPosRel", args = 1, }
self.key_events.GotoPrevPos = { { "Up" }, event = "GotoPosRel", args = -1, }
end
if Device:hasKeyboard() and not Device.k3_alt_plus_key_kernel_translated then
self.key_events.GotoFirst = { { "1" }, event = "GotoPercent", args = 0, }
self.key_events.Goto11 = { { "2" }, event = "GotoPercent", args = 11, }
self.key_events.Goto22 = { { "3" }, event = "GotoPercent", args = 22, }
self.key_events.Goto33 = { { "4" }, event = "GotoPercent", args = 33, }
self.key_events.Goto44 = { { "5" }, event = "GotoPercent", args = 44, }
self.key_events.Goto55 = { { "6" }, event = "GotoPercent", args = 55, }
self.key_events.Goto66 = { { "7" }, event = "GotoPercent", args = 66, }
self.key_events.Goto77 = { { "8" }, event = "GotoPercent", args = 77, }
self.key_events.Goto88 = { { "9" }, event = "GotoPercent", args = 88, }
self.key_events.Goto99 = { { "0" }, event = "GotoPercent", args = 100, }
end
end
ReaderPaging.onPhysicalKeyboardConnected = ReaderPaging.registerKeyEvents
function ReaderPaging:onReaderReady()
self:setupTouchZones()
-- Statistics plugin updates the footer later, if enabled
if not (self.ui.statistics and self.ui.statistics.settings.is_enabled) then
self.view.footer:onUpdateFooter()
end
end
function ReaderPaging:setupTouchZones()
if not Device:isTouchDevice() then return end
local forward_zone, backward_zone = self.view:getTapZones()
self.ui:registerTouchZones({
{
id = "tap_forward",
ges = "tap",
screen_zone = forward_zone,
handler = function()
if G_reader_settings:nilOrFalse("page_turns_disable_tap") then
return self:onGotoViewRel(1)
end
end,
},
{
id = "tap_backward",
ges = "tap",
screen_zone = backward_zone,
handler = function()
if G_reader_settings:nilOrFalse("page_turns_disable_tap") then
return self:onGotoViewRel(-1)
end
end,
},
{
id = "paging_swipe",
ges = "swipe",
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
handler = function(ges) return self:onSwipe(nil, ges) end,
},
{
id = "paging_pan",
ges = "pan",
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
handler = function(ges) return self:onPan(nil, ges) end,
},
{
id = "paging_pan_release",
ges = "pan_release",
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
handler = function(ges) return self:onPanRelease(nil, ges) end,
},
})
end
function ReaderPaging:onReadSettings(config)
self.page_positions = config:readSetting("page_positions") or {}
self:_gotoPage(config:readSetting("last_page") or 1)
self.flipping_zoom_mode = config:readSetting("flipping_zoom_mode") or "page"
self.flipping_scroll_mode = config:isTrue("flipping_scroll_mode")
end
function ReaderPaging:onSaveSettings()
--- @todo only save current_page page position
self.ui.doc_settings:saveSetting("page_positions", self.page_positions)
self.ui.doc_settings:saveSetting("last_page", self:getTopPage())
self.ui.doc_settings:saveSetting("percent_finished", self:getLastPercent())
self.ui.doc_settings:saveSetting("flipping_zoom_mode", self.flipping_zoom_mode)
self.ui.doc_settings:saveSetting("flipping_scroll_mode", self.flipping_scroll_mode)
end
function ReaderPaging:getLastProgress()
return self:getTopPage()
end
function ReaderPaging:getLastPercent()
if self.current_page > 0 and self.number_of_pages > 0 then
return self.current_page/self.number_of_pages
end
end
function ReaderPaging:onColorRenderingUpdate()
self.ui.document:updateColorRendering()
UIManager:setDirty(self.view.dialog, "partial")
end
--[[
Set reading position on certain page
Page position is a fractional number ranging from 0 to 1, indicating the read
percentage on certain page. With the position information on each page whenever
users change font size, page margin or line spacing or close and reopen the
book, the page view will be roughly the same.
--]]
function ReaderPaging:setPagePosition(page, pos)
logger.dbg("set page position", pos)
self.page_positions[page] = pos ~= 0 and pos or nil
self.ui:handleEvent(Event:new("PagePositionUpdated"))
end
--[[
Get reading position on certain page
--]]
function ReaderPaging:getPagePosition(page)
-- Page number ought to be integer, somehow I notice that with
-- fractional page number the reader runs silently well, but the
-- number won't fit to retrieve page position.
page = math.floor(page)
logger.dbg("get page position", self.page_positions[page])
return self.page_positions[page] or 0
end
function ReaderPaging:onTogglePageFlipping()
if self.bookmark_flipping_mode then
-- do nothing if we're in bookmark flipping mode
return
end
self.view.flipping_visible = not self.view.flipping_visible
self.page_flipping_mode = self.view.flipping_visible
self.flipping_page = self.current_page
if self.page_flipping_mode then
self:updateOriginalPage(self.current_page)
self:enterFlippingMode()
else
self:updateOriginalPage(nil)
self:exitFlippingMode()
end
self.ui:handleEvent(Event:new("SetHinting", not self.page_flipping_mode))
self.ui:handleEvent(Event:new("ReZoom"))
UIManager:setDirty(self.view.dialog, "partial")
end
function ReaderPaging:onToggleBookmarkFlipping()
self.bookmark_flipping_mode = not self.bookmark_flipping_mode
if self.bookmark_flipping_mode then
self.orig_flipping_mode = self.view.flipping_visible
self.view.flipping_visible = true
self.bm_flipping_orig_page = self.current_page
self:enterFlippingMode()
else
self.view.flipping_visible = self.orig_flipping_mode
self:exitFlippingMode()
self:_gotoPage(self.bm_flipping_orig_page)
end
self.ui:handleEvent(Event:new("SetHinting", not self.bookmark_flipping_mode))
self.ui:handleEvent(Event:new("ReZoom"))
UIManager:setDirty(self.view.dialog, "partial")
end
function ReaderPaging:enterFlippingMode()
self.orig_reflow_mode = self.view.document.configurable.text_wrap
self.orig_scroll_mode = self.view.page_scroll
self.orig_zoom_mode = self.view.zoom_mode
logger.dbg("store zoom mode", self.orig_zoom_mode)
self.view.document.configurable.text_wrap = 0
self.view.page_scroll = self.flipping_scroll_mode
Input.disable_double_tap = false
self.ui:handleEvent(Event:new("EnterFlippingMode", self.flipping_zoom_mode))
end
function ReaderPaging:exitFlippingMode()
Input.disable_double_tap = true
self.view.document.configurable.text_wrap = self.orig_reflow_mode
self.view.page_scroll = self.orig_scroll_mode
self.flipping_zoom_mode = self.view.zoom_mode
self.flipping_scroll_mode = self.view.page_scroll
logger.dbg("restore zoom mode", self.orig_zoom_mode)
self.ui:handleEvent(Event:new("ExitFlippingMode", self.orig_zoom_mode))
end
function ReaderPaging:updateOriginalPage(page)
self.original_page = page
end
function ReaderPaging:updateFlippingPage(page)
self.flipping_page = page
end
function ReaderPaging:pageFlipping(flipping_page, flipping_ges)
local whole = self.number_of_pages
local steps = #self.flip_steps
local stp_proportion = flipping_ges.distance / Screen:getWidth()
local abs_proportion = flipping_ges.distance / Screen:getHeight()
local direction = BD.flipDirectionIfMirroredUILayout(flipping_ges.direction)
if direction == "east" then
self:_gotoPage(flipping_page - self.flip_steps[math.ceil(steps*stp_proportion)])
elseif direction == "west" then
self:_gotoPage(flipping_page + self.flip_steps[math.ceil(steps*stp_proportion)])
elseif direction == "south" then
self:_gotoPage(flipping_page - math.floor(whole*abs_proportion))
elseif direction == "north" then
self:_gotoPage(flipping_page + math.floor(whole*abs_proportion))
end
UIManager:setDirty(self.view.dialog, "partial")
end
function ReaderPaging:bookmarkFlipping(flipping_page, flipping_ges)
local direction = BD.flipDirectionIfMirroredUILayout(flipping_ges.direction)
if direction == "east" then
self.ui:handleEvent(Event:new("GotoPreviousBookmark", flipping_page))
elseif direction == "west" then
self.ui:handleEvent(Event:new("GotoNextBookmark", flipping_page))
end
UIManager:setDirty(self.view.dialog, "partial")
end
function ReaderPaging:enterSkimMode()
if self.view.document.configurable.text_wrap ~= 0 or self.view.page_scroll or self.view.zoom_mode ~= "page" then
self.skim_backup = {
text_wrap = self.view.document.configurable.text_wrap,
page_scroll = self.view.page_scroll,
zoom_mode = self.view.zoom_mode,
current_page = self.current_page,
location = self:getBookLocation(),
visible_area = self.visible_area,
}
self.view.document.configurable.text_wrap = 0
self.view.page_scroll = false
self.ui.zooming:onSetZoomMode("page")
self.ui.zooming:onReZoom()
end
end
function ReaderPaging:exitSkimMode()
if self.skim_backup then
self.view.document.configurable.text_wrap = self.skim_backup.text_wrap
self.view.page_scroll = self.skim_backup.page_scroll
self.ui.zooming:onSetZoomMode(self.skim_backup.zoom_mode)
self.ui.zooming:onReZoom()
if self.current_page == self.skim_backup.current_page then
-- if SkimToWidget is closed on the start page, restore exact location
self.current_page = 0 -- do not emit extra PageUpdate event
self:onRestoreBookLocation(self.skim_backup.location)
end
self.visible_area = self.skim_backup.visible_area
self.skim_backup = nil
end
end
function ReaderPaging:onScrollSettingsUpdated(scroll_method, inertial_scroll_enabled, scroll_activation_delay_ms)
self.scroll_method = scroll_method
self.scroll_activation_delay = time.ms(scroll_activation_delay_ms)
if inertial_scroll_enabled then
self.ui.scrolling:setInertialScrollCallbacks(
function(distance) -- do_scroll_callback
if not self.ui.document then
return false
end
UIManager.currently_scrolling = true
local top_page, top_position = self:getTopPage(), self:getTopPosition()
self:onPanningRel(distance)
return not (top_page == self:getTopPage() and top_position == self:getTopPosition())
end,
function() -- scroll_done_callback
UIManager.currently_scrolling = false
UIManager:setDirty(self.view.dialog, "partial")
end
)
else
self.ui.scrolling:setInertialScrollCallbacks(nil, nil)
end
end
function ReaderPaging:onSwipe(_, ges)
if self._pan_has_scrolled then
-- We did some panning but released after a short amount of time,
-- so this gesture ended up being a Swipe - and this swipe was
-- not handled by the other modules (so, not opening the menus).
-- Do as :onPanRelease() and ignore this swipe.
self:onPanRelease() -- no arg, so we know there we come from here
return true
else
self._pan_started = false
UIManager.currently_scrolling = false
self._pan_page_states_to_restore = nil
end
local direction = BD.flipDirectionIfMirroredUILayout(ges.direction)
if self.bookmark_flipping_mode then
self:bookmarkFlipping(self.current_page, ges)
return true
elseif self.page_flipping_mode and self.original_page then
self:_gotoPage(self.original_page)
return true
elseif direction == "west" then
if G_reader_settings:nilOrFalse("page_turns_disable_swipe") then
if self.view.inverse_reading_order then
self:onGotoViewRel(-1)
else
self:onGotoViewRel(1)
end
return true
end
elseif direction == "east" then
if G_reader_settings:nilOrFalse("page_turns_disable_swipe") then
if self.view.inverse_reading_order then
self:onGotoViewRel(1)
else
self:onGotoViewRel(-1)
end
return true
end
end
end
function ReaderPaging:onPan(_, ges)
if self.bookmark_flipping_mode then
return true
elseif self.page_flipping_mode then
if self.view.zoom_mode == "page" then
self:pageFlipping(self.flipping_page, ges)
else
self.view:PanningStart(-ges.relative.x, -ges.relative.y)
end
elseif ges.direction == "north" or ges.direction == "south" then
if ges.mousewheel_direction and not self.view.page_scroll then
-- Mouse wheel generates a Pan event: in page mode, move one
-- page per event. Scroll mode is handled in the 'else' branch
-- and use the wheeled distance.
self:onGotoViewRel(-1 * ges.mousewheel_direction)
elseif self.view.page_scroll then
if not self._pan_started then
self._pan_started = true
-- Re-init state variables
self._pan_has_scrolled = false
self._pan_prev_relative_y = 0
self._pan_to_scroll_later = 0
self._pan_real_last_time = 0
if ges.mousewheel_direction then
self._pan_activation_time = false
else
self._pan_activation_time = ges.time + self.scroll_activation_delay
end
-- We will restore the previous position if this pan
-- ends up being a swipe or a multiswipe
-- Somehow, accumulating the distances scrolled in a self._pan_dist_to_restore
-- so we can scroll these back may not always put us back to the original
-- position (possibly because of these page_states?). It's safer
-- to remember the original page_states and restore that. We can keep
-- a reference to the original table as onPanningRel() will have this
-- table replaced.
self._pan_page_states_to_restore = self.view.page_states
end
local scroll_now = false
if self._pan_activation_time and ges.time >= self._pan_activation_time then
self._pan_activation_time = false -- We can go on, no need to check again
end
if not self._pan_activation_time and ges.time - self._pan_real_last_time >= self.pan_interval then
scroll_now = true
self._pan_real_last_time = ges.time
end
local scroll_dist = 0
if self.scroll_method == self.ui.scrolling.SCROLL_METHOD_CLASSIC then
-- Scroll by the distance the finger moved since last pan event,
-- having the document follows the finger
scroll_dist = self._pan_prev_relative_y - ges.relative.y
self._pan_prev_relative_y = ges.relative.y
if not self._pan_has_scrolled then
-- Avoid checking this for each pan, no need once we have scrolled
if self.ui.scrolling:cancelInertialScroll() or self.ui.scrolling:cancelledByTouch() then
-- If this pan or its initial touch did cancel some inertial scrolling,
-- ignore activation delay to allow continuous scrolling
self._pan_activation_time = false
scroll_now = true
self._pan_real_last_time = ges.time
end
end
self.ui.scrolling:accountManualScroll(scroll_dist, ges.time)
elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_TURBO then
-- Legacy scrolling "buggy" behaviour, that can actually be nice
-- Scroll by the distance from the initial finger position, this distance
-- controlling the speed of the scrolling)
if scroll_now then
scroll_dist = -ges.relative.y
end
-- We don't accumulate in _pan_to_scroll_later
elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_ON_RELEASE then
self._pan_to_scroll_later = -ges.relative.y
if scroll_now then
self._pan_has_scrolled = true -- so we really apply it later
end
scroll_dist = 0
scroll_now = false
end
if scroll_now then
local dist = self._pan_to_scroll_later + scroll_dist
self._pan_to_scroll_later = 0
if dist ~= 0 then
self._pan_has_scrolled = true
UIManager.currently_scrolling = true
self:onPanningRel(dist)
end
else
self._pan_to_scroll_later = self._pan_to_scroll_later + scroll_dist
end
end
end
return true
end
function ReaderPaging:onPanRelease(_, ges)
if self.page_flipping_mode then
if self.view.zoom_mode == "page" then
self:updateFlippingPage(self.current_page)
else
self.view:PanningStop()
end
else
if self._pan_has_scrolled and self._pan_to_scroll_later ~= 0 then
self:onPanningRel(self._pan_to_scroll_later)
end
self._pan_started = false
self._pan_page_states_to_restore = nil
UIManager.currently_scrolling = false
if self._pan_has_scrolled then
self._pan_has_scrolled = false
-- Don't do any inertial scrolling if pan events come from
-- a mousewheel (which may have itself some inertia)
if (ges and ges.from_mousewheel) or not self.ui.scrolling:startInertialScroll() then
UIManager:setDirty(self.view.dialog, "partial")
end
end
end
end
function ReaderPaging:onHandledAsSwipe()
if self._pan_started then
-- Restore original position as this pan we've started handling
-- has ended up being a multiswipe or handled as a swipe to open
-- top or bottom menus
if self._pan_has_scrolled then
self.view.page_states = self._pan_page_states_to_restore
self:_gotoPage(self.view.page_states[#self.view.page_states].page, "scrolling")
UIManager:setDirty(self.view.dialog, "ui")
end
self._pan_page_states_to_restore = nil
self._pan_started = false
self._pan_has_scrolled = false
UIManager.currently_scrolling = false
end
return true
end
function ReaderPaging:onZoomModeUpdate(new_mode)
-- we need to remember zoom mode to handle page turn event
self.zoom_mode = new_mode
end
function ReaderPaging:onPageUpdate(new_page_no, orig_mode)
self.current_page = new_page_no
if self.view.page_scroll and orig_mode ~= "scrolling" then
self.ui:handleEvent(Event:new("InitScrollPageStates", orig_mode))
end
end
function ReaderPaging:onViewRecalculate(visible_area, page_area)
-- we need to remember areas to handle page turn event
self.visible_area = visible_area:copy()
self.page_area = page_area
end
function ReaderPaging:onGotoPercent(percent)
logger.dbg("goto document offset in percent:", percent)
local dest = math.floor(self.number_of_pages * percent * (1/100))
if dest < 1 then dest = 1 end
if dest > self.number_of_pages then
dest = self.number_of_pages
end
self:_gotoPage(dest)
return true
end
function ReaderPaging:onGotoViewRel(diff, no_page_turn)
-- When called via a key event, the second arg is a key object (table), not used here.
no_page_turn = no_page_turn == true and true or nil
-- ReaderSearch calls with no_page_turn = true.
-- In that case, don't turn page if it would happen, and return ret=nil.
local ret
if self.view.page_scroll then
ret = self:onScrollPageRel(diff, no_page_turn)
else
ret = self:onGotoPageRel(diff, no_page_turn)
end
self:setPagePosition(self:getTopPage(), self:getTopPosition())
return ret
end
function ReaderPaging:onGotoPosRel(diff)
if self.view.page_scroll then
self:onPanningRel(100*diff)
else
self:onGotoPageRel(diff)
end
self:setPagePosition(self:getTopPage(), self:getTopPosition())
return true
end
function ReaderPaging:onPanningRel(diff)
if self.view.page_scroll then
self:onScrollPanRel(diff)
end
self:setPagePosition(self:getTopPage(), self:getTopPosition())
return true
end
-- Used by ReaderBack & ReaderLink.
function ReaderPaging:getBookLocation()
local ctx = self.view:getViewContext()
if ctx then
-- We need a copy, as we're getting references to
-- objects ReaderPaging/ReaderView may still modify
local current_location = util.tableDeepCopy(ctx)
return current_location
end
end
function ReaderPaging:onRestoreBookLocation(saved_location)
if not saved_location or not saved_location[1] then
return
end
-- We need a copy, as we will assign this to ReaderView.state
-- which when modified would change our instance on ReaderLink.location_stack
local ctx = util.tableDeepCopy(saved_location)
if self.view.page_scroll then
if self.view:restoreViewContext(ctx) then
self:_gotoPage(saved_location[1].page, "scrolling")
else
-- If context is unusable (not from scroll mode), trigger
-- this to go at least to its page and redraw it
self.ui:handleEvent(Event:new("PageUpdate", saved_location[1].page))
end
else
-- gotoPage may emit PageUpdate event, which will trigger recalculate
-- in ReaderView and resets the view context. So we need to call
-- restoreViewContext after gotoPage.
-- But if we're restoring to the same page, it will not emit
-- PageUpdate event - so we need to do it for a correct redrawing
local send_PageUpdate = saved_location[1].page == self.current_page
self:_gotoPage(saved_location[1].page)
if not self.view:restoreViewContext(ctx) then
-- If context is unusable (not from page mode), also
-- send PageUpdate event to go to its page and redraw it
send_PageUpdate = true
end
if send_PageUpdate then
self.ui:handleEvent(Event:new("PageUpdate", saved_location[1].page))
end
end
self:setPagePosition(self:getTopPage(), self:getTopPosition())
-- In some cases (same page, different offset), doing the above
-- might not redraw the screen. Ensure it is.
UIManager:setDirty(self.view.dialog, "partial")
return true
end
--[[
Get read percentage on current page.
--]]
function ReaderPaging:getTopPosition()
if self.view.page_scroll then
local state = self.view.page_states[1]
return (state.visible_area.y - state.page_area.y)/state.page_area.h
else
return 0
end
end
--[[
Get page number of the page drawn at the very top part of the screen.
--]]
function ReaderPaging:getTopPage()
if self.view.page_scroll then
local state = self.view.page_states[1]
return state and state.page or self.current_page
else
return self.current_page
end
end
function ReaderPaging:onInitScrollPageStates(orig_mode)
logger.dbg("init scroll page states", orig_mode)
if self.view.page_scroll and self.view.state.page then
self.orig_page = self.current_page
self.view.page_states = {}
local blank_area = Geom:new()
blank_area:setSizeTo(self.view.visible_area)
while blank_area.h > 0 do
local offset = Geom:new()
-- calculate position in current page
if self.current_page == self.orig_page then
local page_area = self.view:getPageArea(
self.view.state.page,
self.view.state.zoom,
self.view.state.rotation)
offset.y = page_area.h * self:getPagePosition(self.current_page)
end
local state = self:getNextPageState(blank_area, offset)
table.insert(self.view.page_states, state)
if blank_area.h > 0 then
blank_area.h = blank_area.h - self.view.page_gap.height
end
if blank_area.h > 0 then
local next_page = self.ui.document:getNextPage(self.current_page)
if next_page == 0 then break end -- end of document reached
self:_gotoPage(next_page, "scrolling")
end
end
self:_gotoPage(self.orig_page, "scrolling")
end
return true
end
function ReaderPaging:onUpdateScrollPageRotation(rotation)
for _, state in ipairs(self.view.page_states) do
state.rotation = rotation
end
return true
end
function ReaderPaging:onUpdateScrollPageGamma(gamma)
for _, state in ipairs(self.view.page_states) do
state.gamma = gamma
end
return true
end
function ReaderPaging:getNextPageState(blank_area, image_offset)
local page_area = self.view:getPageArea(
self.view.state.page,
self.view.state.zoom,
self.view.state.rotation)
local visible_area = Geom:new{x = 0, y = 0}
visible_area.w, visible_area.h = blank_area.w, blank_area.h
visible_area.x, visible_area.y = page_area.x, page_area.y
visible_area = visible_area:shrinkInside(page_area, image_offset.x, image_offset.y)
-- shrink blank area by the height of visible area
blank_area.h = blank_area.h - visible_area.h
local page_offset = Geom:new{x = self.view.state.offset.x, y = 0}
if blank_area.w > page_area.w then
page_offset:offsetBy((blank_area.w - page_area.w) / 2, 0)
end
return {
page = self.view.state.page,
zoom = self.view.state.zoom,
rotation = self.view.state.rotation,
gamma = self.view.state.gamma,
offset = page_offset,
visible_area = visible_area,
page_area = page_area,
}
end
function ReaderPaging:getPrevPageState(blank_area, image_offset)
local page_area = self.view:getPageArea(
self.view.state.page,
self.view.state.zoom,
self.view.state.rotation)
local visible_area = Geom:new{x = 0, y = 0}
visible_area.w, visible_area.h = blank_area.w, blank_area.h
visible_area.x = page_area.x
visible_area.y = page_area.y + page_area.h - visible_area.h
visible_area = visible_area:shrinkInside(page_area, image_offset.x, image_offset.y)
-- shrink blank area by the height of visible area
blank_area.h = blank_area.h - visible_area.h
local page_offset = Geom:new{x = self.view.state.offset.x, y = 0}
if blank_area.w > page_area.w then
page_offset:offsetBy((blank_area.w - page_area.w) / 2, 0)
end
return {
page = self.view.state.page,
zoom = self.view.state.zoom,
rotation = self.view.state.rotation,
gamma = self.view.state.gamma,
offset = page_offset,
visible_area = visible_area,
page_area = page_area,
}
end
function ReaderPaging:updateTopPageState(state, blank_area, offset)
local visible_area = Geom:new{
x = state.visible_area.x,
y = state.visible_area.y,
w = blank_area.w,
h = blank_area.h,
}
if self.ui.document:getNextPage(state.page) == 0 then -- last page
visible_area:offsetWithin(state.page_area, offset.x, offset.y)
else
visible_area = visible_area:shrinkInside(state.page_area, offset.x, offset.y)
end
-- shrink blank area by the height of visible area
blank_area.h = blank_area.h - visible_area.h
state.visible_area = visible_area
end
function ReaderPaging:updateBottomPageState(state, blank_area, offset)
local visible_area = Geom:new{
x = state.page_area.x,
y = state.visible_area.y + state.visible_area.h - blank_area.h,
w = blank_area.w,
h = blank_area.h,
}
if self.ui.document:getPrevPage(state.page) == 0 then -- first page
visible_area:offsetWithin(state.page_area, offset.x, offset.y)
else
visible_area = visible_area:shrinkInside(state.page_area, offset.x, offset.y)
end
-- shrink blank area by the height of visible area
blank_area.h = blank_area.h - visible_area.h
state.visible_area = visible_area
end
function ReaderPaging:genPageStatesFromTop(top_page_state, blank_area, offset)
-- Offset should always be greater than 0
-- otherwise if offset is less than 0 the height of blank area will be
-- larger than 0 even if page area is much larger than visible area,
-- which will trigger the drawing of next page leaving part of current
-- page undrawn. This should also be true for generating from bottom.
if offset.y < 0 then offset.y = 0 end
self:updateTopPageState(top_page_state, blank_area, offset)
local page_states = {}
if top_page_state.visible_area.h > 0 then
-- offset does not scroll pass top_page_state
table.insert(page_states, top_page_state)
end
local state
local current_page = top_page_state.page
while blank_area.h > 0 do
blank_area.h = blank_area.h - self.view.page_gap.height
if blank_area.h > 0 then
current_page = self.ui.document:getNextPage(current_page)
if current_page == 0 then break end -- end of document reached
self:_gotoPage(current_page, "scrolling")
state = self:getNextPageState(blank_area, Geom:new())
table.insert(page_states, state)
end
end
return page_states
end
function ReaderPaging:genPageStatesFromBottom(bottom_page_state, blank_area, offset)
-- scroll up offset should always be less than 0
if offset.y > 0 then offset.y = 0 end
-- find out number of pages need to be removed from current view
self:updateBottomPageState(bottom_page_state, blank_area, offset)
local page_states = {}
if bottom_page_state.visible_area.h > 0 then
table.insert(page_states, bottom_page_state)
end
-- fill up current view from bottom to top
local state
local current_page = bottom_page_state.page
while blank_area.h > 0 do
blank_area.h = blank_area.h - self.view.page_gap.height
if blank_area.h > 0 then
current_page = self.ui.document:getPrevPage(current_page)
if current_page == 0 then break end -- start of document reached
self:_gotoPage(current_page, "scrolling")
state = self:getPrevPageState(blank_area, Geom:new())
table.insert(page_states, 1, state)
end
end
if current_page == 0 then
-- We reached the start of document: we may have truncated too much
-- of the bottom page while scrolling up.
-- Re-generate everything with first page starting at top
offset = Geom:new{x = 0, y = 0}
blank_area:setSizeTo(self.view.visible_area)
local first_page_state = page_states[1]
first_page_state.visible_area.y = 0 -- anchor first page at top
return self:genPageStatesFromTop(first_page_state, blank_area, offset)
end
return page_states
end
function ReaderPaging:onScrollPanRel(diff)
if diff == 0 then return true end
logger.dbg("pan relative height:", diff)
local offset = Geom:new{x = 0, y = diff}
local blank_area = Geom:new()
blank_area:setSizeTo(self.view.visible_area)
local new_page_states
if diff > 0 then
-- pan to scroll down
local first_page_state = copyPageState(self.view.page_states[1])
new_page_states = self:genPageStatesFromTop(
first_page_state, blank_area, offset)
elseif diff < 0 then
local last_page_state = copyPageState(
self.view.page_states[#self.view.page_states])
new_page_states = self:genPageStatesFromBottom(
last_page_state, blank_area, offset)
end
if #new_page_states == 0 then
-- if we are already at the top of first page or bottom of the last
-- page, new_page_states will be empty, in this case, nothing to update
return true
end
self.view.page_states = new_page_states
-- update current pageno to the very last part in current view
self:_gotoPage(self.view.page_states[#self.view.page_states].page,
"scrolling")
UIManager:setDirty(self.view.dialog, "partial")
return true
end
function ReaderPaging:onScrollPageRel(page_diff, no_page_turn)
if no_page_turn then return end -- see ReaderPaging:onGotoViewRel
if page_diff == 0 then return true end
if page_diff > 1 or page_diff < -1 then
-- More than 1 page, don't bother with how far we've scrolled.
self:onGotoRelativePage(Math.round(page_diff))
return true
elseif page_diff > 0 then
-- page down, last page should be moved to top
local last_page_state = table.remove(self.view.page_states)
local last_visible_area = last_page_state.visible_area
if self.ui.document:getNextPage(last_page_state.page) == 0 and
last_visible_area.y + last_visible_area.h >= last_page_state.page_area.h then
table.insert(self.view.page_states, last_page_state)
self.ui:handleEvent(Event:new("EndOfBook"))
return true
end
local blank_area = Geom:new()
blank_area:setSizeTo(self.view.visible_area)
local overlap = self.overlap
local offset = Geom:new{
x = 0,
y = last_visible_area.h - overlap
}
self.view.page_states = self:genPageStatesFromTop(last_page_state, blank_area, offset)
elseif page_diff < 0 then
-- page up, first page should be moved to bottom
local blank_area = Geom:new()
blank_area:setSizeTo(self.view.visible_area)
local overlap = self.overlap
local first_page_state = table.remove(self.view.page_states, 1)
local offset = Geom:new{
x = 0,
y = -first_page_state.visible_area.h + overlap
}
self.view.page_states = self:genPageStatesFromBottom(
first_page_state, blank_area, offset)
end
-- update current pageno to the very last part in current view
self:_gotoPage(self.view.page_states[#self.view.page_states].page, "scrolling")
UIManager:setDirty(self.view.dialog, "partial")
return true
end
function ReaderPaging:onGotoPageRel(diff, no_page_turn)
-- HACK GEMINI: FORCE PAGE TURN FOR IMAGES (NO SCROLL)
-- Si ce n'est pas du texte redistribuable (donc c'est une image/manga)
if not self.ui.document.info.is_reflowable then
-- On calcule la page cible directement
local new_page = self.current_page + diff
if new_page >= 1 and new_page <= self.number_of_pages then
if no_page_turn then return true end
self:_gotoPage(new_page)
return true
end
end
-- FIN HACK GEMINI (Si on continue, on tombe dans la logique standard)
logger.dbg("goto relative page:", diff)
local new_va = self.visible_area:copy()
local x_pan_off, y_pan_off = 0, 0
local right_to_left = self.ui.document.configurable.writing_direction and self.ui.document.configurable.writing_direction > 0
local bottom_to_top = self.ui.zooming.zoom_bottom_to_top
local h_progress = 1 - self.ui.zooming.zoom_overlap_h * (1/100)
local v_progress = 1 - self.ui.zooming.zoom_overlap_v * (1/100)
local old_va = self.visible_area
local old_page = self.current_page
local x, y, w, h = "x", "y", "w", "h"
local x_diff = diff
local y_diff = diff
-- Adjust directions according to settings
if self.ui.zooming.zoom_direction_vertical then -- invert axes
y, x, h, w = x, y, w, h
h_progress, v_progress = v_progress, h_progress
if right_to_left then
x_diff, y_diff = -x_diff, -y_diff
end
if bottom_to_top then
x_diff = -x_diff
end
elseif bottom_to_top then
y_diff = -y_diff
end
if right_to_left then
x_diff = -x_diff
end
if self.zoom_mode ~= "free" then
x_pan_off = Math.roundAwayFromZero(self.visible_area[w] * h_progress * x_diff)
y_pan_off = Math.roundAwayFromZero(self.visible_area[h] * v_progress * y_diff)
end
-- Auxiliary functions to (as much as possible) keep things clear
-- If going backwards (diff < 0) "end" is equivalent to "beginning", "next" to "previous";
-- in column mode, "line" is equivalent to "column".
local function at_end(axis)
-- returns true if we're at the end of line (axis = x) or page (axis = y)
local len, _diff
if axis == x then
len, _diff = w, x_diff
else
len, _diff = h, y_diff
end
return old_va[axis] + old_va[len] + _diff > self.page_area[axis] + self.page_area[len]
or old_va[axis] + _diff < self.page_area[axis]
end
local function goto_end(axis, _diff)
-- updates view area to the end of line (axis = x) or page (axis = y)
local len = axis == x and w or h
_diff = _diff or (axis == x and x_diff or y_diff)
new_va[axis] = _diff > 0
and old_va[axis] + self.page_area[len] - old_va[len]
or self.page_area[axis]
end
local function goto_next_line()
new_va[y] = old_va[y] + y_pan_off
goto_end(x, -x_diff)
end
local function goto_next_page()
local new_page
if self.ui.document:hasHiddenFlows() then
local forward = diff > 0
local pdiff = forward and math.ceil(diff) or math.ceil(-diff)
new_page = self.current_page
for i=1, pdiff do
local test_page = forward and self.ui.document:getNextPage(new_page)
or self.ui.document:getPrevPage(new_page)
if test_page == 0 then -- start or end of document reached
if forward then
new_page = self.number_of_pages + 1 -- to trigger EndOfBook below
else
new_page = 0
end
break
end
new_page = test_page
end
else
new_page = self.current_page + diff
end
if new_page > self.number_of_pages then
if no_page_turn then return true end
self.ui:handleEvent(Event:new("EndOfBook"))
goto_end(y)
goto_end(x)
elseif new_page > 0 then
if no_page_turn then return true end
-- Be sure that the new and old view areas are reset so that no value is carried over to next page.
-- Without this, we would have panned_y = new_va.y - old_va.y > 0, and panned_y will be added to the next page's y direction.
-- This occurs when the current page has a y > 0 position (for example, a cropped page) and can fit the whole page height,
-- while the next page needs scrolling in the height.
self:_gotoPage(new_page)
new_va = self.visible_area:copy()
old_va = self.visible_area
goto_end(y, -y_diff)
goto_end(x, -x_diff)
else
goto_end(x)
end
end
-- Move the view area towards line end
new_va[x] = old_va[x] + x_pan_off
new_va[y] = old_va[y]
local prev_page = self.current_page
-- Handle cases when the view area gets out of page boundaries
local would_turn_page
if not self.page_area:contains(new_va) then
if not at_end(x) then
goto_end(x)
else
goto_next_line()
if not self.page_area:contains(new_va) then
if not at_end(y) then
goto_end(y)
else
would_turn_page = goto_next_page()
end
end
end
end
if no_page_turn and would_turn_page then return end -- see ReaderPaging:onGotoViewRel
if self.current_page == prev_page then
-- Page number haven't changed when panning inside a page,
-- but time may: keep the footer updated
self.view.footer:onUpdateFooter(self.view.footer_visible)
end
-- signal panning update
local panned_x, panned_y = math.floor(new_va.x - old_va.x), math.floor(new_va.y - old_va.y)
self.view:PanningUpdate(panned_x, panned_y)
-- Update dim area in ReaderView
if self.view.page_overlap_enable then
if self.current_page ~= old_page then
self.view.dim_area:clear()
else
-- We're post PanningUpdate, recompute via self.visible_area instead of new_va for accuracy, it'll have been updated via ViewRecalculate
panned_x, panned_y = math.floor(self.visible_area.x - old_va.x), math.floor(self.visible_area.y - old_va.y)
self.view.dim_area.h = self.visible_area.h - math.abs(panned_y)
self.view.dim_area.w = self.visible_area.w - math.abs(panned_x)
if panned_y < 0 then
self.view.dim_area.y = self.visible_area.h - self.view.dim_area.h
else
self.view.dim_area.y = 0
end
if panned_x < 0 then
self.view.dim_area.x = self.visible_area.w - self.view.dim_area.w
else
self.view.dim_area.x = 0
end
end
end
return true
end
function ReaderPaging:onRedrawCurrentPage()
self.ui:handleEvent(Event:new("PageUpdate", self.current_page))
return true
end
-- wrapper for bounds checking
function ReaderPaging:_gotoPage(number, orig_mode)
if number == self.current_page or not number then
-- update footer even if we stay on the same page (like when
-- viewing the bottom part of a page from a top part view)
self.view.footer:onUpdateFooter(self.view.footer_visible)
return true
end
if number > self.number_of_pages then
logger.warn("page number too high: "..number.."!")
number = self.number_of_pages
elseif number < 1 then
logger.warn("page number too low: "..number.."!")
number = 1
end
-- this is an event to allow other controllers to be aware of this change
self.ui:handleEvent(Event:new("PageUpdate", number, orig_mode))
return true
end
function ReaderPaging:onGotoPage(number, pos)
self:setPagePosition(number, 0)
self:_gotoPage(number)
if pos then
local rect_p = Geom:new{ x = pos.x or 0, y = pos.y or 0 }
local rect_s = Geom:new(rect_p):copy()
rect_s:transformByScale(self.view.state.zoom)
if self.view.page_scroll then
self:onScrollPanRel(rect_s.y - self.view.page_area.y)
else
self.view:PanningUpdate(rect_s.x - self.view.visible_area.x, rect_s.y - self.view.visible_area.y)
end
elseif number == self.current_page then
-- gotoPage emits this event only if the page changes
self.ui:handleEvent(Event:new("PageUpdate", self.current_page))
end
return true
end
function ReaderPaging:onGotoRelativePage(number)
local new_page = self.current_page
local test_page = new_page
local forward = number > 0
for i=1, math.abs(number) do
test_page = forward and self.ui.document:getNextPage(test_page)
or self.ui.document:getPrevPage(test_page)
if test_page == 0 then -- start or end of document reached
break
end
new_page = test_page
end
self:_gotoPage(new_page)
return true
end
function ReaderPaging:onGotoPercentage(percentage)
if percentage < 0 then percentage = 0 end
if percentage > 1 then percentage = 1 end
self:_gotoPage(math.floor(percentage*self.number_of_pages))
return true
end
-- These might need additional work to behave fine in scroll
-- mode, and other zoom modes than Fit page
function ReaderPaging:onGotoNextChapter()
local pageno = self.current_page
local new_page = self.ui.toc:getNextChapter(pageno)
if new_page then
self.ui.link:addCurrentLocationToStack()
self:onGotoPage(new_page)
end
return true
end
function ReaderPaging:onGotoPrevChapter()
local pageno = self.current_page
local new_page = self.ui.toc:getPreviousChapter(pageno)
if new_page then
self.ui.link:addCurrentLocationToStack()
self:onGotoPage(new_page)
end
return true
end
function ReaderPaging:onReflowUpdated()
self.ui:handleEvent(Event:new("RedrawCurrentPage"))
self.ui:handleEvent(Event:new("RestoreZoomMode"))
self.ui:handleEvent(Event:new("InitScrollPageStates"))
end
function ReaderPaging:onToggleReflow()
self.view.document.configurable.text_wrap = bit.bxor(self.view.document.configurable.text_wrap, 1)
self:onReflowUpdated()
end
return ReaderPaging