Adds ReaderPageMap, to optionally show source pages numbers

bump crengine: support for EPUB3 nav toc and page maps
Includes:
- Fix lvRect:isRectInside(rc) with 0-width or 0-height rect
- TOC: parse EPUB3 nav toc, fallback to spine when no toc
- Parse and cache various hardcopy page list maps
- epub.css: hide EPUB3 <span epub:type="pagebreak"> content
cre.cpp: add a few PageMap helper functions.

Adds ReaderPageMap which will add a new menu (under TOC and
Bookmarks) that will allow:
- to list source page numbers (like a TOC)
- to show visible page labels in the right margin
- to use these source page numbers in the footer, the TOC,
  the GoTo and SkimTo widgets, and to use the source page
  number in the standard bookmark and highlight initial text.
reviewable/pr6013/r1
poire-z 4 years ago
parent 1cb3be324a
commit 026140f809

@ -1 +1 @@
Subproject commit 7a5c9e1110912c196e478344ec4e1edacb88c061
Subproject commit 4ffbe12ee71cbf3fa4d1d5673e23c515372f4a4d

@ -221,7 +221,11 @@ function ReaderBookmark:onShowBookmark()
local page = v.page
-- for CREngine, bookmark page is xpointer
if not self.ui.document.info.has_pages then
page = self.ui.document:getPageFromXPointer(page)
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
page = self.ui.pagemap:getXPointerPageLabel(page, true)
else
page = self.ui.document:getPageFromXPointer(page)
end
end
if v.text == nil or v.text == "" then
v.text = T(_("Page %1 %2 @ %3"), page, v.notes, v.datetime)

@ -176,6 +176,11 @@ local footerTextGeneratorMap = {
end,
page_progress = function(footer)
if footer.pageno then
if footer.ui.pagemap and footer.ui.pagemap:wantsPageLabels() then
-- (Page labels might not be numbers)
return ("%s / %s"):format(footer.ui.pagemap:getCurrentPageLabel(true),
footer.ui.pagemap:getLastPageLabel(true))
end
return ("%d / %d"):format(footer.pageno, footer.pages)
elseif footer.position then
return ("%d / %d"):format(footer.position, footer.doc_height)

@ -4,6 +4,7 @@ local InputDialog = require("ui/widget/inputdialog")
local SkimToWidget = require("apps/reader/skimtowidget")
local UIManager = require("ui/uimanager")
local _ = require("gettext")
local T = require("ffi/util").template
local ReaderGoto = InputContainer:new{
goto_menu_title = _("Go to"),
@ -50,9 +51,17 @@ function ReaderGoto:onShowGotoDialog()
-- only CreDocument has this method
curr_page = self.document:getCurrentPage()
end
local input_hint
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
input_hint = T("@%1 (%2 - %3)", self.ui.pagemap:getCurrentPageLabel(true),
self.ui.pagemap:getFirstPageLabel(true),
self.ui.pagemap:getLastPageLabel(true))
else
input_hint = T("@%1 (1 - %2)", curr_page, self.document:getPageCount())
end
self.goto_dialog = InputDialog:new{
title = dialog_title,
input_hint = "@"..curr_page.." (1 - "..self.document:getPageCount()..")",
input_hint = input_hint,
buttons = {
{
{
@ -113,7 +122,16 @@ function ReaderGoto:gotoPage()
if relative_sign == "+" or relative_sign == "-" then
self.ui:handleEvent(Event:new("GotoRelativePage", number))
else
self.ui:handleEvent(Event:new("GotoPage", number))
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
number = self.ui.pagemap:getRenderedPageNumber(page_number, true)
if number then -- found
self.ui:handleEvent(Event:new("GotoPage", number))
else
return -- avoid self:close()
end
else
self.ui:handleEvent(Event:new("GotoPage", number))
end
end
self:close()
end

@ -0,0 +1,418 @@
local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
local Font = require("ui/font")
local FrameContainer = require("ui/widget/container/framecontainer")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local InputContainer = require("ui/widget/container/inputcontainer")
local Menu = require("ui/widget/menu")
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local OverlapGroup = require("ui/widget/overlapgroup")
local TextBoxWidget = require("ui/widget/textboxwidget")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
local Screen = Device.screen
local T = require("ffi/util").template
local _ = require("gettext")
local ReaderPageMap = InputContainer:new{
label_font_face = "ffont",
label_default_font_size = 14,
-- Black so it's readable (and non-gray-flashing on GloHD)
label_color = Blitbuffer.COLOR_BLACK,
show_page_labels = nil,
use_page_labels = nil,
_mirroredUI = BD.mirroredUILayout(),
}
function ReaderPageMap:init()
self.has_pagemap = false
self.container = nil
self.max_left_label_width = 0
self.max_right_label_width = 0
self.label_font_size = G_reader_settings:readSetting("pagemap_label_font_size")
or self.label_default_font_size
self.use_textbox_widget = nil
self.initialized = false
self.ui:registerPostInitCallback(function()
self:_postInit()
end)
end
function ReaderPageMap:_postInit()
self.initialized = true
if self.ui.document.info.has_pages then
return
end
if not self.ui.document:hasPageMap() then
return
end
self.has_pagemap = true
self:resetLayout()
self.ui.menu:registerToMainMenu(self)
self.view:registerViewModule("pagemap", self)
end
function ReaderPageMap:resetLayout()
if not self.initialized then
return
end
if self[1] then
self[1]:free()
self[1] = nil
end
if not self.show_page_labels then
return
end
self.container = OverlapGroup:new{
dimen = Screen:getSize(),
-- Pages in 2-page mode are not mirrored, so we'll
-- have to handle any mirroring tweak ourselves
allow_mirroring = false,
}
self[1] = self.container
-- Get some metric for label min width
self.label_face = Font:getFace(self.label_font_face, self.label_font_size)
local textw = TextWidget:new{
text = " ",
face = self.label_face,
}
self.space_width = textw:getWidth()
textw:setText("8")
self.number_width = textw:getWidth()
textw:free()
self.min_label_width = self.space_width * 2 + self.number_width
end
function ReaderPageMap:onReadSettings(config)
local h_margins = config:readSetting("copt_h_page_margins") or
G_reader_settings:readSetting("copt_h_page_margins") or
DCREREADER_CONFIG_H_MARGIN_SIZES_MEDIUM
self.max_left_label_width = Screen:scaleBySize(h_margins[1])
self.max_right_label_width = Screen:scaleBySize(h_margins[2])
self.show_page_labels = config:readSetting("pagemap_show_page_labels")
if self.show_page_labels == nil then
self.show_page_labels = G_reader_settings:nilOrTrue("pagemap_show_page_labels")
end
self.use_page_labels = config:readSetting("pagemap_use_page_labels")
if self.use_page_labels == nil then
self.use_page_labels = G_reader_settings:isTrue("pagemap_use_page_labels")
end
end
function ReaderPageMap:onSetPageMargins(margins)
if not self.has_pagemap then
return
end
self.max_left_label_width = Screen:scaleBySize(margins[1])
self.max_right_label_width = Screen:scaleBySize(margins[3])
self:resetLayout()
end
function ReaderPageMap:cleanPageLabel(label)
-- Cleanup page label, that may contain some noise (as they
-- were meant to be shown in a list, like a TOC)
label = label:gsub("[Pp][Aa][Gg][Ee]%s*", "") -- remove leading "Page " from "Page 123"
return label
end
function ReaderPageMap:updateVisibleLabels()
-- This might be triggered before PostInitCallback is
if not self.initialized then
return
end
if not self.has_pagemap then
return
end
if not self.show_page_labels then
return
end
self.container:clear()
local page_labels = self.ui.document:getPageMapVisiblePageLabels()
local footer_height = (self.view.footer_visible and 1 or 0) * self.view.footer:getHeight()
local max_y = Screen:getHeight() - footer_height
local last_label_bottom_y = 0
for _, page in ipairs(page_labels) do
local in_left_margin = self._mirroredUI
if self.ui.document:getVisiblePageCount() > 1 then
-- Pages in 2-page mode are not mirrored, so we'll
-- have to handle any mirroring tweak ourselves
in_left_margin = page.screen_page == 1
end
local max_label_width = in_left_margin and self.max_left_label_width or self.max_right_label_width
if max_label_width < self.min_label_width then
max_label_width = self.min_label_width
end
local label_width = max_label_width - 2 * self.space_width -- one space to screen edge, one to content
local text = self:cleanPageLabel(page.label)
local label_widget = TextBoxWidget:new{
text = text,
width = label_width,
face = self.label_face,
line_height = 0, -- no additional line height
fgcolor = self.label_color,
alignment = not in_left_margin and "right",
alignment_strict = true,
}
local label_height = label_widget:getTextHeight()
local frame = FrameContainer:new{
bordersize = 0,
padding = 0,
padding_left = in_left_margin and self.space_width,
padding_right = not in_left_margin and self.space_width,
label_widget,
allow_mirroring = false,
}
local offset_x = in_left_margin and 0 or Screen:getWidth() - frame:getSize().w
local offset_y = page.screen_y
if offset_y < last_label_bottom_y then
-- Avoid consecutive labels to overwrite themselbes
offset_y = last_label_bottom_y
end
if offset_y + label_height > max_y then
-- Push label up so it's fully above footer
offset_y = max_y - label_height
end
last_label_bottom_y = offset_y + label_height
frame.overlap_offset = {offset_x, offset_y}
table.insert(self.container, frame)
end
end
-- Events that may change page draw offset, and might need visible labels
-- to be updated to get their correct screen y
ReaderPageMap.onPageUpdate = ReaderPageMap.updateVisibleLabels
ReaderPageMap.onPosUpdate = ReaderPageMap.updateVisibleLabels
ReaderPageMap.onChangeViewMode = ReaderPageMap.updateVisibleLabels
ReaderPageMap.onSetStatusLine = ReaderPageMap.updateVisibleLabels
function ReaderPageMap:onShowPageList()
-- build up item_table
local page_list = self.ui.document:getPageMap()
for k, v in ipairs(page_list) do
v.text = v.label
v.mandatory = v.page
end
local pl_menu = Menu:new{
title = _("Reference page numbers list"),
item_table = page_list,
is_borderless = true,
is_popout = false,
width = Screen:getWidth(),
height = Screen:getHeight(),
cface = Font:getFace("x_smallinfofont"),
perpage = G_reader_settings:readSetting("items_per_page") or 14,
line_color = require("ffi/blitbuffer").COLOR_WHITE,
single_line = true,
on_close_ges = {
GestureRange:new{
ges = "two_finger_swipe",
range = Geom:new{
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight(),
},
direction = BD.flipDirectionIfMirroredUILayout("east")
}
}
}
self.pagelist_menu = CenterContainer:new{
dimen = Screen:getSize(),
covers_fullscreen = true, -- hint for UIManager:_repaint()
pl_menu,
}
-- buid up menu widget method as closure
local pagemap = self
function pl_menu:onMenuChoice(item)
pagemap.ui.link:addCurrentLocationToStack()
pagemap.ui.rolling:onGotoXPointer(item.xpointer)
end
pl_menu.close_callback = function()
UIManager:close(self.pagelist_menu)
end
pl_menu.show_parent = self.pagelist_menu
self.refresh = function()
pl_menu:updateItems()
end
UIManager:show(self.pagelist_menu)
return true
end
function ReaderPageMap:wantsPageLabels()
return self.has_pagemap and self.use_page_labels
end
function ReaderPageMap:getCurrentPageLabel(clean_label)
-- Note: in scroll mode with PDF, when multiple pages are shown on
-- the screen, the advertized page number is the greatest page number
-- among the pages shown (so, the page number of the partial page
-- shown at bottom of screen).
-- For consistency, getPageMapCurrentPageLabel() returns the last page
-- label shown in the view if there are more than one (or the previous
-- one if there is none).
local label = self.ui.document:getPageMapCurrentPageLabel()
return clean_label and self:cleanPageLabel(label) or label
end
function ReaderPageMap:getFirstPageLabel(clean_label)
local label = self.ui.document:getPageMapFirstPageLabel()
return clean_label and self:cleanPageLabel(label) or label
end
function ReaderPageMap:getLastPageLabel(clean_label)
local label = self.ui.document:getPageMapLastPageLabel()
return clean_label and self:cleanPageLabel(label) or label
end
function ReaderPageMap:getXPointerPageLabel(xp, clean_label)
local label = self.ui.document:getPageMapXPointerPageLabel(xp)
return clean_label and self:cleanPageLabel(label) or label
end
function ReaderPageMap:getRenderedPageNumber(page_label, cleaned)
-- Only used from ReaderGoTo. As page_label is a string, no
-- way to use a binary search: do a full scan of the PageMap
-- here in Lua, even if it's not cheap.
local page_list = self.ui.document:getPageMap()
for k, v in ipairs(page_list) do
local label = cleaned and self:cleanPageLabel(v.label) or v.label
if label == page_label then
return v.page
end
end
end
function ReaderPageMap:addToMainMenu(menu_items)
menu_items.page_map = {
-- @translators This and the other related ones refer to alternate page numbers provided in some EPUB books, that usually reference page numbers in a specific hardcopy edition of the book.
text = _("Reference pages"),
sub_item_table ={
{
-- @translators This shows the <dc:source> in the EPUB that usually tells which hardcopy edition the reference page numbers refers to.
text = _("Reference source info"),
enabled_func = function() return self.ui.document:getPageMapSource() ~= nil end,
callback = function()
local text = T(_("Source (book hardcopy edition) of reference page numbers:\n\n%1"),
self.ui.document:getPageMapSource())
local InfoMessage = require("ui/widget/infomessage")
local infomsg = InfoMessage:new{
text = text,
}
UIManager:show(infomsg)
end,
keep_menu_open = true,
},
{
text = _("Reference page numbers list"),
callback = function()
self:onShowPageList()
end,
},
{
text = _("Use reference page numbers"),
checked_func = function() return self.use_page_labels end,
callback = function()
self.use_page_labels = not self.use_page_labels
self.ui.doc_settings:saveSetting("pagemap_use_page_labels", self.use_page_labels)
-- Reset a few stuff that may use page labels
self.ui.toc:resetToc()
self.ui.view.footer:updateFooter()
UIManager:setDirty(self.view.dialog, "partial")
end,
hold_callback = function(touchmenu_instance)
local use_page_labels = G_reader_settings:isTrue("pagemap_use_page_labels")
UIManager:show(MultiConfirmBox:new{
text = use_page_labels and _("The default (★) for newly opened books that have a reference page numbers map is to use these reference page numbers instead of the renderer page numbers.\n\nWould you like to change it?")
or _("The default (★) for newly opened books that have a reference page numbers map is to not use these reference page numbers and keep using the renderer page numbers.\n\nWould you like to change it?"),
choice1_text_func = function()
return use_page_labels and _("Renderer") or _("Renderer (★)")
end,
choice1_callback = function()
G_reader_settings:saveSetting("pagemap_use_page_labels", false)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
choice2_text_func = function()
return use_page_labels and _("Reference (★)") or _("Reference")
end,
choice2_callback = function()
G_reader_settings:saveSetting("pagemap_use_page_labels", true)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
})
end,
separator = true,
},
{
text = _("Show reference page labels in margin"),
checked_func = function() return self.show_page_labels end,
callback = function()
self.show_page_labels = not self.show_page_labels
self.ui.doc_settings:saveSetting("pagemap_show_page_labels", self.show_page_labels)
self:resetLayout()
self:updateVisibleLabels()
UIManager:setDirty(self.view.dialog, "partial")
end,
hold_callback = function(touchmenu_instance)
local show_page_labels = G_reader_settings:nilOrTrue("pagemap_show_page_labels")
UIManager:show(MultiConfirmBox:new{
text = show_page_labels and _("The default (★) for newly opened books that have a reference page numbers map is to show reference page number labels in the margin.\n\nWould you like to change it?")
or _("The default (★) for newly opened books that have a reference page numbers map is to not show reference page number labels in the margin.\n\nWould you like to change it?"),
choice1_text_func = function()
return show_page_labels and _("Hide") or _("Hide (★)")
end,
choice1_callback = function()
G_reader_settings:saveSetting("pagemap_show_page_labels", false)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
choice2_text_func = function()
return show_page_labels and _("Show (★)") or _("Show")
end,
choice2_callback = function()
G_reader_settings:saveSetting("pagemap_show_page_labels", true)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
})
end,
},
{
text_func = function()
return T(_("Page labels font size (%1)"), self.label_font_size)
end,
callback = function(touchmenu_instance)
local SpinWidget = require("ui/widget/spinwidget")
local spin_w = SpinWidget:new{
width = Screen:getWidth() * 0.6,
value = self.label_font_size,
value_min = 8,
value_max = 20,
default_value = self.label_default_font_size,
ok_text = _("Set size"),
title_text = _("Page labels font size"),
callback = function(spin)
self.label_font_size = spin.value
G_reader_settings:saveSetting("pagemap_label_font_size", self.label_font_size)
if touchmenu_instance then touchmenu_instance:updateItems() end
self:resetLayout()
self:updateVisibleLabels()
UIManager:setDirty(self.view.dialog, "partial")
end,
}
UIManager:show(spin_w)
end,
keep_menu_open = true,
},
},
}
end
return ReaderPageMap

@ -790,7 +790,6 @@ function ReaderRolling:onUpdatePos()
-- that were triggering a full repaint of crengine (so, the needed
-- rerendering) before updatePos() is called.
UIManager:scheduleIn(0.1, function () self:updatePos() end)
return true
end
function ReaderRolling:updatePos()
@ -837,7 +836,6 @@ function ReaderRolling:onChangeViewMode()
self:_gotoXPointer(self.xpointer)
end)
end
return true
end
function ReaderRolling:onRedrawCurrentView()

@ -390,6 +390,9 @@ function ReaderToc:onShowToc()
if v.orig_page then -- bogus page fixed: show original page number
v.mandatory = T("(%1) %2", v.orig_page, v.page)
end
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
v.mandatory = self.ui.pagemap:getXPointerPageLabel(v.xpointer)
end
end
end

@ -558,7 +558,6 @@ Tap to dismiss.]]),
dismiss_callback = refresh_callback,
})
end
return true
end
return ReaderTypeset

@ -38,6 +38,7 @@ local ReaderHyphenation = require("apps/reader/modules/readerhyphenation")
local ReaderKoptListener = require("apps/reader/modules/readerkoptlistener")
local ReaderLink = require("apps/reader/modules/readerlink")
local ReaderMenu = require("apps/reader/modules/readermenu")
local ReaderPageMap = require("apps/reader/modules/readerpagemap")
local ReaderPanning = require("apps/reader/modules/readerpanning")
local ReaderRotation = require("apps/reader/modules/readerrotation")
local ReaderPaging = require("apps/reader/modules/readerpaging")
@ -319,6 +320,12 @@ function ReaderUI:init()
view = self.view,
ui = self
})
-- pagemap controller
self:registerModule("pagemap", ReaderPageMap:new{
dialog = self.dialog,
view = self.view,
ui = self
})
self.disable_double_tap = G_reader_settings:readSetting("disable_double_tap") ~= false
end
-- back location stack

@ -66,6 +66,11 @@ function SkimToWidget:init()
self.curr_page = self.ui:getCurrentPage()
self.page_count = self.document:getPageCount()
local curr_page_display = tostring(self.curr_page)
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
curr_page_display = self.ui.pagemap:getCurrentPageLabel(true)
end
local ticks_candidates = {}
if self.ui.toc then
local max_level = self.ui.toc:getMaxDepth()
@ -170,7 +175,7 @@ function SkimToWidget:init()
end,
}
self.current_page_text = Button:new{
text = tostring(self.curr_page),
text = curr_page_display,
bordersize = 0,
margin = self.button_margin,
radius = 0,
@ -336,7 +341,11 @@ function SkimToWidget:update()
self.curr_page = self.page_count
end
self.progress_bar.percentage = self.curr_page / self.page_count
self.current_page_text:setText(tostring(self.curr_page), self.current_page_text.width)
local curr_page_display = tostring(self.curr_page)
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
curr_page_display = self.ui.pagemap:getCurrentPageLabel(true)
end
self.current_page_text:setText(curr_page_display, self.current_page_text.width)
end
function SkimToWidget:addOriginToLocationStack(add_current)

@ -877,12 +877,45 @@ function CreDocument:buildAlternativeToc()
self._document:buildAlternativeToc()
end
function CreDocument:hasPageMap()
return self._document:hasPageMap()
end
function CreDocument:getPageMap()
return self._document:getPageMap()
end
function CreDocument:getPageMapSource()
return self._document:getPageMapSource()
end
function CreDocument:getPageMapCurrentPageLabel()
return self._document:getPageMapCurrentPageLabel()
end
function CreDocument:getPageMapFirstPageLabel()
return self._document:getPageMapFirstPageLabel()
end
function CreDocument:getPageMapLastPageLabel()
return self._document:getPageMapLastPageLabel()
end
function CreDocument:getPageMapXPointerPageLabel(xp)
return self._document:getPageMapXPointerPageLabel(xp)
end
function CreDocument:getPageMapVisiblePageLabels()
return self._document:getPageMapVisiblePageLabels()
end
function CreDocument:register(registry)
registry:addProvider("azw", "application/vnd.amazon.mobi8-ebook", self, 90)
registry:addProvider("chm", "application/vnd.ms-htmlhelp", self, 90)
registry:addProvider("doc", "application/msword", self, 90)
registry:addProvider("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", self, 90)
registry:addProvider("epub", "application/epub+zip", self, 100)
registry:addProvider("epub3", "application/epub+zip", self, 100)
registry:addProvider("fb2", "application/fb2", self, 90)
registry:addProvider("fb2.zip", "application/zip", self, 90)
registry:addProvider("fb3", "application/fb3", self, 90)
@ -1184,6 +1217,8 @@ function CreDocument:setupCallCache()
elseif name == "getScreenPositionFromXPointer" then cache_by_tag = true
elseif name == "getXPointer" then cache_by_tag = true
elseif name == "isXPointerInCurrentPage" then cache_by_tag = true
elseif name == "getPageMapCurrentPageLabel" then cache_by_tag = true
elseif name == "getPageMapVisiblePageLabels" then cache_by_tag = true
-- Assume all remaining get* can have their results
-- cached globally by function arguments

@ -15,6 +15,7 @@ local order = {
"bookmarks",
"toggle_bookmark",
"bookmark_browsing_mode",
"page_map",
"----------------------------",
"go_to",
"skim_to",

Loading…
Cancel
Save