2
0
mirror of https://github.com/koreader/koreader synced 2024-11-13 19:11:25 +00:00
koreader/frontend/document/document.lua
chrox 8c9751744e fix #1064 by adding timestamp of document in cache key
so that when document is modified the persistent cache will
be invalidated automatically because the cache key will not
be matched. There is no perfermance overhead here at all. We
even don't need to check the modification time of the cache item
on disk, because the name of the on disk cache is a md5sum of the
cacheitem key, now the filename of the cache files contains the
modification time information.
If the document is modified since one rendered page is cached to disk,
the cache key won't match the cache file. And the cache file will
be discarded without the need to open the cache file or to check
the modification time of the cache file itself.
2014-10-30 11:05:26 +08:00

357 lines
10 KiB
Lua

local TileCacheItem = require("document/tilecacheitem")
local DrawContext = require("ffi/drawcontext")
local Configurable = require("configurable")
local Blitbuffer = require("ffi/blitbuffer")
local lfs = require("libs/libkoreader-lfs")
local CacheItem = require("cacheitem")
local Geom = require("ui/geometry")
local Math = require("optmath")
local Cache = require("cache")
local DEBUG = require("dbg")
--[[
This is an abstract interface to a document
]]--
local Document = {
-- file name
file = nil,
links = {},
GAMMA_NO_GAMMA = 1.0,
-- override bbox from orignal page's getUsedBBox
bbox = {},
-- flag to show whether the document was opened successfully
is_open = false,
-- flag to show that the document needs to be unlocked by a password
is_locked = false,
-- flag to show that the document is edited and needs to write back to disk
is_edited = false,
}
function Document:new(o)
local o = o or {}
setmetatable(o, self)
self.__index = self
if o._init then o:_init() end
if o.init then o:init() end
return o
end
-- base document initialization should be called on each document init
function Document:_init()
self.configurable = Configurable:new()
self.info = {
-- whether the document is pageable
has_pages = false,
-- whether words can be provided
has_words = false,
-- whether hyperlinks can be provided
has_hyperlinks = false,
-- whether (native to format) annotations can be provided
has_annotations = false,
-- whether pages can be rotated
is_rotatable = false,
number_of_pages = 0,
-- if not pageable, length of the document in pixels
doc_height = 0,
-- other metadata
title = "",
author = "",
date = ""
}
end
-- override this method to open a document
function Document:init()
end
-- this might be overridden by a document implementation
function Document:unlock(password)
-- return true instead when the password provided unlocked the document
return false
end
-- this might be overridden by a document implementation
function Document:close()
local DocumentRegistry = require("document/documentregistry")
if self.is_open and DocumentRegistry:closeDocument(self.file) == 0 then
self.is_open = false
self._document:close()
end
end
-- check if document is edited and needs to write to disk
function Document:isEdited()
return self.is_edited
end
-- discard change will set is_edited flag to false and implematation of Document
-- should check the is_edited flag before writing document
function Document:discardChange()
self.is_edited = false
end
-- this might be overridden by a document implementation
function Document:getNativePageDimensions(pageno)
local hash = "pgdim|"..self.file.."|"..pageno
local cached = Cache:check(hash)
if cached then
return cached[1]
end
local page = self._document:openPage(pageno)
local page_size_w, page_size_h = page:getSize(self.dc_null)
local page_size = Geom:new{ w = page_size_w, h = page_size_h }
Cache:insert(hash, CacheItem:new{ page_size })
page:close()
return page_size
end
function Document:_readMetadata()
self.mod_time = lfs.attributes(self.file, "modification")
self.info.number_of_pages = self._document:getPages()
return true
end
function Document:getPageCount()
return self.info.number_of_pages
end
-- calculates page dimensions
function Document:getPageDimensions(pageno, zoom, rotation)
local native_dimen = self:getNativePageDimensions(pageno):copy()
if rotation == 90 or rotation == 270 then
-- switch orientation
native_dimen.w, native_dimen.h = native_dimen.h, native_dimen.w
end
native_dimen:scaleBy(zoom)
--DEBUG("dimen for pageno", pageno, "zoom", zoom, "rotation", rotation, "is", native_dimen)
return native_dimen
end
function Document:getPageBBox(pageno)
local bbox = self.bbox[pageno] -- exact
if bbox ~= nil then
--DEBUG("bbox from", pageno)
return bbox
else
local oddEven = Math.oddEven(pageno)
bbox = self.bbox[oddEven] -- odd/even
end
if bbox ~= nil then -- last used up to this page
--DEBUG("bbox from", oddEven)
return bbox
else
for i = 0,pageno do
bbox = self.bbox[ pageno - i ]
if bbox ~= nil then
--DEBUG("bbox from", pageno - i)
return bbox
end
end
end
if bbox == nil then -- fallback bbox
bbox = self:getUsedBBox(pageno)
--DEBUG("bbox from ORIGINAL page")
end
--DEBUG("final bbox", bbox)
return bbox
end
--[[
This method returns pagesize if bbox is corrupted
--]]
function Document:getUsedBBoxDimensions(pageno, zoom, rotation)
local bbox = self:getPageBBox(pageno)
-- clipping page bbox
if bbox.x0 < 0 then bbox.x0 = 0 end
if bbox.y0 < 0 then bbox.y0 = 0 end
if bbox.x1 < 0 then bbox.x1 = 0 end
if bbox.y1 < 0 then bbox.y1 = 0 end
local ubbox_dimen = nil
if (bbox.x0 >= bbox.x1) or (bbox.y0 >= bbox.y1) then
-- if document's bbox info is corrupted, we use the page size
ubbox_dimen = self:getPageDimensions(pageno, zoom, rotation)
else
ubbox_dimen = Geom:new{
x = bbox.x0,
y = bbox.y0,
w = bbox.x1 - bbox.x0,
h = bbox.y1 - bbox.y0,
}
if zoom ~= 1 then
ubbox_dimen:transformByScale(zoom)
end
end
return ubbox_dimen
end
function Document:getToc()
return self._document:getToc()
end
function Document:getPageLinks(pageno)
return nil
end
function Document:getLinkFromPosition(pageno, pos)
return nil
end
function Document:getTextBoxes(pageno)
return nil
end
function Document:getOCRWord(pageno, rect)
return nil
end
function Document:getCoverPageImage()
return nil
end
function Document:getFullPageHash(pageno, zoom, rotation, gamma, render_mode)
return "renderpg|"..self.file.."|"..self.mod_time.."|"..pageno.."|"
..zoom.."|"..rotation.."|"..gamma.."|"..render_mode
end
function Document:renderPage(pageno, rect, zoom, rotation, gamma, render_mode)
local hash = self:getFullPageHash(pageno, zoom, rotation, gamma, render_mode)
local page_size = self:getPageDimensions(pageno, zoom, rotation)
-- this will be the size we actually render
local size = page_size
-- we prefer to render the full page, if it fits into cache
if not Cache:willAccept(size.w * size.h / 2) then
-- whole page won't fit into cache
DEBUG("rendering only part of the page")
-- TODO: figure out how to better segment the page
if not rect then
DEBUG("aborting, since we do not have a specification for that part")
-- required part not given, so abort
return
end
-- only render required part
hash = self:getFullPageHash(pageno, zoom, rotation, gamma, render_mode).."|"..tostring(rect)
size = rect
end
-- prepare cache item with contained blitbuffer
local tile = TileCacheItem:new{
persistent = true,
size = size.w * size.h + 64, -- estimation
excerpt = size,
pageno = pageno,
bb = Blitbuffer.new(size.w, size.h)
}
-- create a draw context
local dc = DrawContext.new()
dc:setRotate(rotation)
-- correction of rotation
if rotation == 90 then
dc:setOffset(page_size.w, 0)
elseif rotation == 180 then
dc:setOffset(page_size.w, page_size.h)
elseif rotation == 270 then
dc:setOffset(0, page_size.h)
end
dc:setZoom(zoom)
if gamma ~= self.GAMMA_NO_GAMMA then
--DEBUG("gamma correction: ", gamma)
dc:setGamma(gamma)
end
-- render
local page = self._document:openPage(pageno)
page:draw(dc, tile.bb, size.x, size.y, render_mode)
page:close()
Cache:insert(hash, tile)
return tile
end
-- a hint for the cache engine to paint a full page to the cache
-- TODO: this should trigger a background operation
function Document:hintPage(pageno, zoom, rotation, gamma, render_mode)
local hash_full_page = self:getFullPageHash(pageno, zoom, rotation, gamma, render_mode)
if not Cache:check(hash_full_page, TileCacheItem) then
DEBUG("hinting page", pageno)
self:renderPage(pageno, nil, zoom, rotation, gamma, render_mode)
end
end
--[[
Draw page content to blitbuffer.
1. find tile in cache
2. if not found, call renderPage
@target: target blitbuffer
@rect: visible_area inside document page
--]]
function Document:drawPage(target, x, y, rect, pageno, zoom, rotation, gamma, render_mode)
local hash_full_page = self:getFullPageHash(pageno, zoom, rotation, gamma, render_mode)
local hash_excerpt = hash_full_page.."|"..tostring(rect)
local tile = Cache:check(hash_full_page, TileCacheItem)
if not tile then
tile = Cache:check(hash_excerpt)
if not tile then
DEBUG("rendering")
tile = self:renderPage(pageno, rect, zoom, rotation, gamma, render_mode)
end
end
DEBUG("now painting", tile, rect)
target:blitFrom(tile.bb,
x, y,
rect.x - tile.excerpt.x,
rect.y - tile.excerpt.y,
rect.w, rect.h)
end
function Document:getPageText(pageno)
-- is this worth caching? not done yet.
local page = self._document:openPage(pageno)
local text = page:getPageText()
page:close()
return text
end
function Document:saveHighlight(pageno, item)
return nil
end
--[[
helper functions
--]]
function Document:logMemoryUsage(pageno)
local status_file = io.open("/proc/self/status", "r")
local log_file = io.open("mem_usage_log.txt", "a+")
local data = -1
if status_file then
for line in status_file:lines() do
local s, n
s, n = line:gsub("VmData:%s-(%d+) kB", "%1")
if n ~= 0 then data = tonumber(s) end
if data ~= -1 then break end
end
status_file:close()
end
if log_file then
if log_file:seek("end") == 0 then -- write the header only once
log_file:write("PAGE\tMEM\n")
end
log_file:write(string.format("%s\t%s\n", pageno, data))
log_file:close()
end
end
return Document