add text highlight in both reflow and non-reflow mode

pull/85/head
chrox 11 years ago
parent 7f53ddacbe
commit 936dfc6fd1

@ -66,7 +66,10 @@ customupdate: all
cp -p README.md COPYING $(KOR_BASE)/{koreader-base,extr} koreader.sh $(LUA_FILES) $(INSTALL_DIR)
$(STRIP) --strip-unneeded $(INSTALL_DIR)/koreader-base $(INSTALL_DIR)/extr
mkdir $(INSTALL_DIR)/data
cp -L koreader-base/$(DJVULIB) $(KOR_BASE)/$(CRELIB) $(KOR_BASE)/$(LUALIB) $(KOR_BASE)/$(K2PDFOPTLIB) $(INSTALL_DIR)/libs
cp -L koreader-base/$(DJVULIB) $(KOR_BASE)/$(CRELIB) \
$(KOR_BASE)/$(LUALIB) $(KOR_BASE)/$(K2PDFOPTLIB) \
$(KOR_BASE)/$(LEPTONICALIB) $(KOR_BASE)/$(TESSERACTLIB) \
$(INSTALL_DIR)/libs
$(STRIP) --strip-unneeded $(INSTALL_DIR)/libs/*
cp -rpL $(KOR_BASE)/data/*.css $(INSTALL_DIR)/data
cp -rpL $(KOR_BASE)/fonts $(INSTALL_DIR)

@ -47,6 +47,25 @@ function validDjvuFile(filename)
return true
end
function DjvuDocument:getPageText(pageno)
if self.configurable.text_wrap == 1 then
return self.koptinterface:getPageText(self, pageno)
else
return self._document:getPageText(pageno)
end
end
function DjvuDocument:getOCRWord(pageno, rect)
if self.configurable.text_wrap == 1 then
return self.koptinterface:getOCRWord(self, pageno, rect)
else
--local page = self._document:openPage(pageno)
--local word = page:getOCRWord(rect)
--page:close()
--return word
end
end
function DjvuDocument:getUsedBBox(pageno)
-- djvu does not support usedbbox, so fake it.
local used = {}

@ -184,6 +184,14 @@ function Document:getToc()
return self._document:getToc()
end
function Document:getPageText(pageno)
return nil
end
function Document:getOCRWord(pageno, rect)
return nil
end
function Document:renderPage(pageno, rect, zoom, rotation, gamma, render_mode)
local hash = "renderpg|"..self.file.."|"..pageno.."|"..zoom.."|"..rotation.."|"..gamma.."|"..render_mode
local page_size = self:getPageDimensions(pageno, zoom, rotation)

@ -1,9 +1,23 @@
require "dbg"
require "cache"
require "ui/geometry"
require "ui/device"
require "ui/reader/readerconfig"
KoptInterface = {}
KoptInterface = {
tessocr_data = "data",
ocr_lang = "eng",
ocr_type = 0, -- default, for more accuracy use 3
}
ContextCacheItem = CacheItem:new{}
function ContextCacheItem:onFree()
if self.kctx.free then
DEBUG("free koptcontext", self.kctx)
self.kctx:free()
end
end
function KoptInterface:waitForContext(kc)
-- if koptcontext is being processed in background thread
@ -12,10 +26,13 @@ function KoptInterface:waitForContext(kc)
DEBUG("waiting for background rendering")
util.usleep(100000)
end
return kc
end
-- get reflow context
function KoptInterface:getKOPTContext(doc, pageno, bbox)
--[[
get reflow context
--]]
function KoptInterface:createContext(doc, pageno, bbox)
-- Now koptcontext keeps track of its dst bitmap reflowed by libk2pdfopt.
-- So there is no need to check background context when creating new context.
local kc = KOPTContext.new()
@ -37,6 +54,7 @@ function KoptInterface:getKOPTContext(doc, pageno, bbox)
kc:setLineSpacing(doc.configurable.line_spacing)
kc:setWordSpacing(doc.configurable.word_spacing)
kc:setBBox(bbox.x0, bbox.y0, bbox.x1, bbox.y1)
if Dbg.is_on then kc:setDebug() end
return kc
end
@ -47,17 +65,6 @@ function KoptInterface:getContextHash(doc, pageno, bbox)
return doc.file.."|"..pageno.."|"..doc.configurable:hash("|").."|"..bbox_hash.."|"..screen_size_hash
end
function KoptInterface:logReflowDuration(pageno, dur)
local file = io.open("reflowlog.txt", "a+")
if file then
if file:seek("end") == 0 then -- write the header only once
file:write("PAGE\tDUR\n")
end
file:write(string.format("%s\t%s\n", pageno, dur))
file:close()
end
end
function KoptInterface:getAutoBBox(doc, pageno)
local bbox = {
x0 = 0, y0 = 0,
@ -68,26 +75,68 @@ function KoptInterface:getAutoBBox(doc, pageno)
local cached = Cache:check(hash)
if not cached then
local page = doc._document:openPage(pageno)
local kc = self:getKOPTContext(doc, pageno, bbox)
local kc = self:createContext(doc, pageno, bbox)
bbox.x0, bbox.y0, bbox.x1, bbox.y1 = page:getAutoBBox(kc)
DEBUG("Auto detected bbox", bbox)
page:close()
Cache:insert(hash, CacheItem:new{ bbox = bbox })
Cache:insert(hash, CacheItem:new{ autobbox = bbox })
return bbox
else
return cached.bbox
return cached.autobbox
end
end
function KoptInterface:getPageText(doc, pageno)
local bbox = doc:getPageBBox(pageno)
local context_hash = self:getContextHash(doc, pageno, bbox)
local hash = "pgtext|"..context_hash
local cached = Cache:check(hash)
if not cached then
local kctx_hash = "kctx|"..context_hash
local cached = Cache:check(kctx_hash)
if cached then
local kc = self:waitForContext(cached.kctx)
local fullwidth, fullheight = kc:getPageDim()
local text = kc:getWordBoxes(0, 0, fullwidth, fullheight)
Cache:insert(hash, CacheItem:new{ pgtext = text })
return text
end
else
return cached.pgtext
end
end
-- get reflowed page dimension from a cached context. wait for background thread
-- if necessary.
function KoptInterface:getReflowedDim(kctx)
self:waitForContext(kctx)
return kctx:getPageDim()
function KoptInterface:getOCRWord(doc, pageno, rect)
local bbox = doc:getPageBBox(pageno)
local context_hash = self:getContextHash(doc, pageno, bbox)
local hash = "ocrword|"..context_hash..rect.x..rect.y..rect.w..rect.h
local cached = Cache:check(hash)
if not cached then
local kctx_hash = "kctx|"..context_hash
local cached = Cache:check(kctx_hash)
if cached then
local kc = self:waitForContext(cached.kctx)
local fullwidth, fullheight = kc:getPageDim()
local ok, word = pcall(
kc.getOCRWord, kc,
self.tessocr_data,
self.ocr_lang,
self.ocr_type,
rect.x, rect.y,
rect.w, rect.h)
Cache:insert(hash, CacheItem:new{ ocrword = word })
return word
end
else
return cached.ocrword
end
end
-- get cached koptcontext for centain page. if context doesn't exist in cache make
-- new context and reflow the src page immediatly.
--[[
get cached koptcontext for centain page. if context doesn't exist in cache make
new context and reflow the src page immediatly, or wait background thread for
reflowed context.
--]]
function KoptInterface:getCachedContext(doc, pageno)
local bbox = doc:getPageBBox(pageno)
local context_hash = self:getContextHash(doc, pageno, bbox)
@ -95,7 +144,7 @@ function KoptInterface:getCachedContext(doc, pageno)
local cached = Cache:check(kctx_hash)
if not cached then
-- If kctx is not cached, create one and get reflowed bmp in foreground.
local kc = self:getKOPTContext(doc, pageno, bbox)
local kc = self:createContext(doc, pageno, bbox)
local page = doc._document:openPage(pageno)
-- reflow page
--local secs, usecs = util.gettime()
@ -107,22 +156,27 @@ function KoptInterface:getCachedContext(doc, pageno)
--self:logReflowDuration(pageno, dur)
local fullwidth, fullheight = kc:getPageDim()
DEBUG("reflowed page", pageno, "fullwidth:", fullwidth, "fullheight:", fullheight)
Cache:insert(kctx_hash, CacheItem:new{ kctx = kc })
Cache:insert(kctx_hash, ContextCacheItem:new{ kctx = kc })
return kc
else
return cached.kctx
-- wait for background thread
return self:waitForContext(cached.kctx)
end
end
-- get reflowed page dimensions
--[[
get reflowed page dimensions
--]]
function KoptInterface:getPageDimensions(doc, pageno, zoom, rotation)
local kctx = self:getCachedContext(doc, pageno)
local fullwidth, fullheight = self:getReflowedDim(kctx)
local kc = self:getCachedContext(doc, pageno)
local fullwidth, fullheight = kc:getPageDim()
return Geom:new{ w = fullwidth, h = fullheight }
end
-- inherited from common document interface
-- render reflowed page into tile cache.
--[[
inherited from common document interface
render reflowed page into tile cache.
--]]
function KoptInterface:renderPage(doc, pageno, rect, zoom, rotation, render_mode)
doc.render_mode = render_mode
local bbox = doc:getPageBBox(pageno)
@ -132,8 +186,8 @@ function KoptInterface:renderPage(doc, pageno, rect, zoom, rotation, render_mode
local cached = Cache:check(renderpg_hash)
if not cached then
-- do the real reflowing if kctx is not been cached yet
local kctx = self:getCachedContext(doc, pageno)
local fullwidth, fullheight = self:getReflowedDim(kctx)
local kc = self:getCachedContext(doc, pageno)
local fullwidth, fullheight = kc:getPageDim()
if not Cache:willAccept(fullwidth * fullheight / 2) then
-- whole page won't fit into cache
error("aborting, since we don't have enough cache for this page")
@ -146,10 +200,8 @@ function KoptInterface:renderPage(doc, pageno, rect, zoom, rotation, render_mode
pageno = pageno,
bb = Blitbuffer.new(fullwidth, fullheight)
}
page:rfdraw(kctx, tile.bb)
page:rfdraw(kc, tile.bb)
page:close()
-- free dst bitmap in kctx as soon as we draw the bitmap to blitbuffer
kctx:free()
Cache:insert(renderpg_hash, tile)
return tile
else
@ -157,29 +209,33 @@ function KoptInterface:renderPage(doc, pageno, rect, zoom, rotation, render_mode
end
end
-- inherited from common document interface
-- render reflowed page into cache in background thread. this method returns immediatly
-- leaving the precache flag on in context. subsequent usage of this context should
-- wait for the precache flag off by calling self:waitForContext(kctx)
--[[
inherited from common document interface
render reflowed page into cache in background thread. this method returns immediatly
leaving the precache flag on in context. subsequent usage of this context should
wait for the precache flag off by calling self:waitForContext(kctx)
--]]
function KoptInterface:hintPage(doc, pageno, zoom, rotation, gamma, render_mode)
local bbox = doc:getPageBBox(pageno)
local context_hash = self:getContextHash(doc, pageno, bbox)
local kctx_hash = "kctx|"..context_hash
local cached = Cache:check(kctx_hash)
if not cached then
local kc = self:getKOPTContext(doc, pageno, bbox)
local kc = self:createContext(doc, pageno, bbox)
local page = doc._document:openPage(pageno)
DEBUG("hinting page", pageno, "in background")
-- reflow will return immediately and running in background thread
kc:setPreCache()
page:reflow(kc, 0)
page:close()
Cache:insert(kctx_hash, CacheItem:new{ kctx = kc })
Cache:insert(kctx_hash, ContextCacheItem:new{ kctx = kc })
end
end
-- inherited from common document interface
-- draw cached tile pixels into target blitbuffer
--[[
inherited from common document interface
draw cached tile pixels into target blitbuffer.
--]]
function KoptInterface:drawPage(doc, target, x, y, rect, pageno, zoom, rotation, render_mode)
local tile = self:renderPage(doc, pageno, rect, zoom, rotation, render_mode)
--DEBUG("now painting", tile, rect)
@ -189,3 +245,17 @@ function KoptInterface:drawPage(doc, target, x, y, rect, pageno, zoom, rotation,
rect.y - tile.excerpt.y,
rect.w, rect.h)
end
--[[
helper functions
--]]
function KoptInterface:logReflowDuration(pageno, dur)
local file = io.open("reflowlog.txt", "a+")
if file then
if file:seek("end") == 0 then -- write the header only once
file:write("PAGE\tDUR\n")
end
file:write(string.format("%s\t%s\n", pageno, dur))
file:close()
end
end

@ -44,6 +44,28 @@ function PdfDocument:unlock(password)
return self:_readMetadata()
end
function PdfDocument:getPageText(pageno)
if self.configurable.text_wrap == 1 then
return self.koptinterface:getPageText(self, pageno)
else
local page = self._document:openPage(pageno)
local text = page:getPageText()
page:close()
return text
end
end
function PdfDocument:getOCRWord(pageno, rect)
if self.configurable.text_wrap == 1 then
return self.koptinterface:getOCRWord(self, pageno, rect)
else
--local page = self._document:openPage(pageno)
--local word = page:getOCRWord(rect)
--page:close()
--return word
end
end
function PdfDocument:getUsedBBox(pageno)
local hash = "pgubbox|"..self.file.."|"..pageno
local cached = Cache:check(hash)

@ -0,0 +1,24 @@
require "ui/device"
ReaderDictionary = EventListener:new{}
function ReaderDictionary:init()
local dev_mod = Device:getModel()
if dev_mod == "KindlePaperWhite" or dev_mod == "KindleTouch" then
require "liblipclua"
self.lipc_handle = lipc.init("com.github.koreader.dictionary")
end
end
function ReaderDictionary:onLookupWord(word)
DEBUG("lookup word:", word)
if self.lipc_handle then
-- start indicator depends on pillow being enabled
self.lipc_handle:set_string_property(
"com.lab126.booklet.kpvbooklet.dict", "lookup", word)
local definitions = self.lipc_handle:get_string_property(
"com.lab126.booklet.kpvbooklet.word", word)
DEBUG("definitions of word:", word, definitions)
end
return true
end

@ -0,0 +1,125 @@
ReaderHighlight = InputContainer:new{}
function ReaderHighlight:init()
if Device:hasKeyboard() then
self.key_events = {
ShowToc = {
{ "." },
doc = _("highlight text") },
}
end
end
function ReaderHighlight:initGesListener()
self.ges_events = {
Tap = {
GestureRange:new{
ges = "tap",
range = Geom:new{
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight()
}
}
},
Hold = {
GestureRange:new{
ges = "hold",
range = Geom:new{
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight()
}
}
},
}
end
function ReaderHighlight:onSetDimensions(dimen)
-- update listening according to new screen dimen
if Device:isTouchDevice() then
self:initGesListener()
end
end
function ReaderHighlight:onTap(arg, ges)
if self.view.highlight.rect then
self.view.highlight.rect = nil
UIManager:setDirty(self.dialog, "partial")
return true
end
end
function ReaderHighlight:onHold(arg, ges)
self.pos = self.view:screenToPageTransform(ges.pos)
DEBUG("hold position in page", self.pos)
local text = self.ui.document:getPageText(self.pos.page)
--DEBUG("page text", text)
if not text or #text == 0 then
DEBUG("no text extracted")
return true
end
self.word_info = self:getWordFromText(text, self.pos)
DEBUG("hold word info in page", self.word_info)
if self.word_info then
local screen_rect = self.view:pageToScreenTransform(self.pos.page, self.word_info.box)
DEBUG("highlight word rect", screen_rect)
if screen_rect then
self.view.highlight.rect = screen_rect
UIManager:setDirty(self.dialog, "partial")
-- if we extracted text directly
if self.word_info.word then
self.ui:handleEvent(Event:new("LookupWord", self.word_info.word))
-- or we will do OCR
else
UIManager:scheduleIn(0.1, function()
local word_box = self.word_info.box
word_box.x = word_box.x - math.floor(word_box.h * 0.02)
word_box.y = word_box.y - math.floor(word_box.h * 0.02)
word_box.w = word_box.w + math.floor(word_box.h * 0.04)
word_box.h = word_box.h + math.floor(word_box.h * 0.04)
local word = self.ui.document:getOCRWord(self.pos.page, word_box)
DEBUG("OCRed word:", word)
self.ui:handleEvent(Event:new("LookupWord", word))
end)
end
end
end
return true
end
function ReaderHighlight:getWordFromText(text, pos)
local function ges_inside(x0, y0, x1, y1)
local x, y = pos.x, pos.y
if x0 ~= nil and y0 ~= nil and x1 ~= nil and y1 ~= nil then
if x0 <= x and y0 <= y and x1 >= x and y1 >= y then
return true
end
end
return false
end
for i = 1, #text do
local l = text[i]
if ges_inside(l.x0, l.y0, l.x1, l.y1) then
--DEBUG("line box", l.x0, l.y0, l.x1, l.y1)
for j = 1, #text[i] do
local w = text[i][j]
if ges_inside(w.x0, w.y0, w.x1, w.y1) then
local box = Geom:new{
x = w.x0, y = w.y0,
w = w.x1 - w.x0,
h = w.y1 - w.y0,
}
return {
word = w.word,
box = box,
}
end -- end if inside word box
end -- end for each word
end -- end if inside line box
end -- end for each line
end

@ -41,3 +41,11 @@ end
function ReaderKoptListener:onFineTuningFontSize(delta)
self.document.configurable.font_size = self.document.configurable.font_size + delta
end
function ReaderKoptListener:onZoomUpdate(zoom)
-- an exceptional case is reflow mode
if self.document.configurable.text_wrap == 1 then
self.view.state.zoom = 1.0
end
end

@ -16,6 +16,10 @@ ReaderView = OverlapGroup:new{
bbox = nil,
},
outer_page_color = 0,
-- hightlight
highlight = {
drawer = "marker" -- show as inverted block instead of underline
},
-- PDF/DjVu continuous paging
page_scroll = nil,
page_bgcolor = 0,
@ -97,6 +101,11 @@ function ReaderView:paintTo(bb, x, y)
)
end
-- draw highlight
if self.highlight.rect then
self:drawHightlight(bb, x, y, self.highlight.rect)
end
-- paint dogear
if self.dogear_visible then
self.dogear:paintTo(bb, x, y)
@ -113,6 +122,32 @@ function ReaderView:paintTo(bb, x, y)
self.ui:handleEvent(Event:new("StopActivityIndicator"))
end
--[[
Given coordinates on the screen return position in original page
]]--
function ReaderView:screenToPageTransform(pos)
if self.ui.document.info.has_pages then
if self.page_scroll then
return self:getScrollPagePosition(pos)
else
return self:getSinglePagePosition(pos)
end
end
end
--[[
Given rectangle in original page return rectangle on the screen
]]--
function ReaderView:pageToScreenTransform(page, rect)
if self.ui.document.info.has_pages then
if self.page_scroll then
return self:getScrollPageRect(page, rect)
else
return self:getSinglePageRect(rect)
end
end
end
function ReaderView:drawPageBackground(bb, x, y)
bb:paintRect(x, y, self.dimen.w, self.dimen.h, self.page_bgcolor)
end
@ -155,6 +190,40 @@ function ReaderView:drawScrollPages(bb, x, y)
end)
end
function ReaderView:getScrollPagePosition(pos)
local x_s, y_s = pos.x, pos.y
local x_p, y_p = nil, nil
for _, state in ipairs(self.page_states) do
if y_s < state.visible_area.h + state.offset.y then
y_p = (state.visible_area.y + y_s - state.offset.y) / state.zoom
x_p = (state.visible_area.x + x_s - state.offset.x) / state.zoom
return {
x = x_p,
y = y_p,
page = state.page,
zoom = state.zoom,
rotation = state.rotation,
}
else
y_s = y_s - state.visible_area.h - self.page_gap.height
end
end
end
function ReaderView:getScrollPageRect(page, rect_p)
local rect_s = Geom:new{}
for _, state in ipairs(self.page_states) do
if page == state.page and state.visible_area:contains(rect_p) then
rect_s.x = rect_s.x + state.offset.x + rect_p.x*state.zoom - state.visible_area.x
rect_s.y = rect_s.y + state.offset.y + rect_p.y*state.zoom - state.visible_area.y
rect_s.w = rect_p.w * state.zoom
rect_s.h = rect_p.h * state.zoom
return rect_s
end
rect_s.y = rect_s.y + state.visible_area.h + self.page_gap.height
end
end
function ReaderView:drawPageGap(bb, x, y)
if self.scroll_mode == "vertical" then
bb:paintRect(x, y, self.dimen.w, self.page_gap.height, self.page_gap.color)
@ -179,6 +248,28 @@ function ReaderView:drawSinglePage(bb, x, y)
end)
end
function ReaderView:getSinglePagePosition(pos)
local x_s, y_s = pos.x, pos.y
return {
x = (self.visible_area.x + x_s - self.state.offset.x) / self.state.zoom,
y = (self.visible_area.y + y_s - self.state.offset.y) / self.state.zoom,
page = self.state.page,
zoom = self.state.zoom,
rotation = self.state.rotation,
}
end
function ReaderView:getSinglePageRect(rect_p)
local rect_s = Geom:new{}
if self.visible_area:contains(rect_p) then
rect_s.x = self.state.offset.x + rect_p.x * self.state.zoom - self.visible_area.x
rect_s.y = self.state.offset.y + rect_p.y * self.state.zoom - self.visible_area.y
rect_s.w = rect_p.w * self.state.zoom
rect_s.h = rect_p.h * self.state.zoom
return rect_s
end
end
function ReaderView:drawPageView(bb, x, y)
self.ui.document:drawCurrentViewByPage(
bb,
@ -197,6 +288,26 @@ function ReaderView:drawScrollView(bb, x, y)
self.state.pos)
end
function ReaderView:drawHightlight(bb, x, y, rect)
-- slightly enlarge the highlight box
-- for better viewing experience
local x = rect.x - rect.h * 0.01
local y = rect.y - rect.h * 0.01
local w = rect.w + rect.h * 0.02
local h = rect.h + rect.h * 0.02
self.highlight.drawer = self.highlight.drawer or "underscore"
if self.highlight.drawer == "underscore" then
self.highlight.line_width = self.highlight.line_width or 2
self.highlight.line_color = self.highlight.line_color or 5
bb:paintRect(x, y+h-1, w,
self.highlight.line_width,
self.highlight.line_color)
elseif self.highlight.drawer == "marker" then
bb:invertRect(x, y, w, h)
end
end
function ReaderView:getPageArea(page, zoom, rotation)
if self.use_bbox then
return self.ui.document:getUsedBBoxDimensions(page, zoom, rotation)

@ -14,8 +14,10 @@ require "ui/reader/readercropping"
require "ui/reader/readerkopt"
require "ui/reader/readercopt"
require "ui/reader/readerhinting"
require "ui/reader/readerhighlight"
require "ui/reader/readerscreenshot"
require "ui/reader/readerfrontlight"
require "ui/reader/readerdictionary"
require "ui/reader/readerhyphenation"
require "ui/reader/readeractivityindicator"
@ -96,6 +98,22 @@ function ReaderUI:init()
ui = self
}
table.insert(self, reader_bm)
-- text highlight
local highlight = ReaderHighlight:new{
dialog = self.dialog,
view = self[1],
ui = self,
document = self.document,
}
table.insert(self, highlight)
-- dictionary
local dict = ReaderDictionary:new{
dialog = self.dialog,
view = self[1],
ui = self,
document = self.document,
}
table.insert(self, dict)
-- screenshot controller
local reader_ss = ReaderScreenshot:new{
dialog = self.dialog,

@ -9,6 +9,9 @@ test -e $PROC_FIVEWAY && echo unlock > $PROC_FIVEWAY
# we're always starting from our working directory
cd /mnt/us/koreader/
# export trained OCR data directory
export TESSDATA_PREFIX="data"
# bind-mount system fonts
if ! grep /mnt/us/koreader/fonts/host /proc/mounts; then
mount -o bind /usr/java/lib/fonts /mnt/us/koreader/fonts/host

Loading…
Cancel
Save