diff --git a/Makefile b/Makefile index cc8b946db..b87f4ff62 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/frontend/JSON.lua b/frontend/JSON.lua new file mode 100644 index 000000000..4be1d95a9 --- /dev/null +++ b/frontend/JSON.lua @@ -0,0 +1,855 @@ +-- -*- coding: utf-8 -*- +-- +-- Copyright 2010-2012 Jeffrey Friedl +-- http://regex.info/blog/ +-- +local VERSION = 20111207.5 -- version history at end of file +local OBJDEF = { VERSION = VERSION } + +-- +-- Simple JSON encoding and decoding in pure Lua. +-- http://www.json.org/ +-- +-- +-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines +-- +-- local lua_value = JSON:decode(raw_json_text) +-- +-- local raw_json_text = JSON:encode(lua_table_or_value) +-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability +-- +-- +-- DECODING +-- +-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines +-- +-- local lua_value = JSON:decode(raw_json_text) +-- +-- If the JSON text is for an object or an array, e.g. +-- { "what": "books", "count": 3 } +-- or +-- [ "Larry", "Curly", "Moe" ] +-- +-- the result is a Lua table, e.g. +-- { what = "books", count = 3 } +-- or +-- { "Larry", "Curly", "Moe" } +-- +-- +-- The encode and decode routines accept an optional second argument, "etc", which is not used +-- during encoding or decoding, but upon error is passed along to error handlers. It can be of any +-- type (including nil). +-- +-- With most errors during decoding, this code calls +-- +-- JSON:onDecodeError(message, text, location, etc) +-- +-- with a message about the error, and if known, the JSON text being parsed and the byte count +-- where the problem was discovered. You can replace the default JSON:onDecodeError() with your +-- own function. +-- +-- The default onDecodeError() merely augments the message with data about the text and the +-- location if known (and if a second 'etc' argument had been provided to decode(), its value is +-- tacked onto the message as well), and then calls JSON.assert(), which itself defaults to Lua's +-- built-in assert(), and can also be overridden. +-- +-- For example, in an Adobe Lightroom plugin, you might use something like +-- +-- function JSON:onDecodeError(message, text, location, etc) +-- LrErrors.throwUserError("Internal Error: invalid JSON data") +-- end +-- +-- or even just +-- +-- function JSON.assert(message) +-- LrErrors.throwUserError("Internal Error: " .. message) +-- end +-- +-- If JSON:decode() is passed a nil, this is called instead: +-- +-- JSON:onDecodeOfNilError(message, nil, nil, etc) +-- +-- and if JSON:decode() is passed HTML instead of JSON, this is called: +-- +-- JSON:onDecodeOfHTMLError(message, text, nil, etc) +-- +-- The use of the fourth 'etc' argument allows stronger coordination between decoding and error +-- reporting, especially when you provide your own error-handling routines. Continuing with the +-- the Adobe Lightroom plugin example: +-- +-- function JSON:onDecodeError(message, text, location, etc) +-- local note = "Internal Error: invalid JSON data" +-- if type(etc) = 'table' and etc.photo then +-- note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName') +-- end +-- LrErrors.throwUserError(note) +-- end +-- +-- : +-- : +-- +-- for i, photo in ipairs(photosToProcess) do +-- : +-- : +-- local data = JSON:decode(someJsonText, { photo = photo }) +-- : +-- : +-- end +-- +-- +-- +-- + +-- DECODING AND STRICT TYPES +-- +-- Because both JSON objects and JSON arrays are converted to Lua tables, it's not normally +-- possible to tell which a Lua table came from, or guarantee decode-encode round-trip +-- equivalency. +-- +-- However, if you enable strictTypes, e.g. +-- +-- JSON = (loadfile "JSON.lua")() --load the routines +-- JSON.strictTypes = true +-- +-- then the Lua table resulting from the decoding of a JSON object or JSON array is marked via Lua +-- metatable, so that when re-encoded with JSON:encode() it ends up as the appropriate JSON type. +-- +-- (This is not the default because other routines may not work well with tables that have a +-- metatable set, for example, Lightroom API calls.) +-- +-- +-- ENCODING +-- +-- JSON = (loadfile "JSON.lua")() -- one-time load of the routines +-- +-- local raw_json_text = JSON:encode(lua_table_or_value) +-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability + +-- On error during encoding, this code calls: +-- +-- JSON:onEncodeError(message, etc) +-- +-- which you can override in your local JSON object. +-- +-- +-- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT +-- +-- assert +-- onDecodeError +-- onDecodeOfNilError +-- onDecodeOfHTMLError +-- onEncodeError +-- +-- If you want to create a separate Lua JSON object with its own error handlers, +-- you can reload JSON.lua or use the :new() method. +-- +--------------------------------------------------------------------------- + + +local author = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json), version " .. tostring(VERSION) .. " ]-" +local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray +local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject + + +function OBJDEF:newArray(tbl) + return setmetatable(tbl or {}, isArray) +end + +function OBJDEF:newObject(tbl) + return setmetatable(tbl or {}, isObject) +end + +local function unicode_codepoint_as_utf8(codepoint) + -- + -- codepoint is a number + -- + if codepoint <= 127 then + return string.char(codepoint) + + elseif codepoint <= 2047 then + -- + -- 110yyyxx 10xxxxxx <-- useful notation from http://en.wikipedia.org/wiki/Utf8 + -- + local highpart = math.floor(codepoint / 0x40) + local lowpart = codepoint - (0x40 * highpart) + return string.char(0xC0 + highpart, + 0x80 + lowpart) + + elseif codepoint <= 65535 then + -- + -- 1110yyyy 10yyyyxx 10xxxxxx + -- + local highpart = math.floor(codepoint / 0x1000) + local remainder = codepoint - 0x1000 * highpart + local midpart = math.floor(remainder / 0x40) + local lowpart = remainder - 0x40 * midpart + + highpart = 0xE0 + highpart + midpart = 0x80 + midpart + lowpart = 0x80 + lowpart + + -- + -- Check for an invalid character (thanks Andy R. at Adobe). + -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070 + -- + if ( highpart == 0xE0 and midpart < 0xA0 ) or + ( highpart == 0xED and midpart > 0x9F ) or + ( highpart == 0xF0 and midpart < 0x90 ) or + ( highpart == 0xF4 and midpart > 0x8F ) + then + return "?" + else + return string.char(highpart, + midpart, + lowpart) + end + + else + -- + -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx + -- + local highpart = math.floor(codepoint / 0x40000) + local remainder = codepoint - 0x40000 * highpart + local midA = math.floor(remainder / 0x1000) + remainder = remainder - 0x1000 * midA + local midB = math.floor(remainder / 0x40) + local lowpart = remainder - 0x40 * midB + + return string.char(0xF0 + highpart, + 0x80 + midA, + 0x80 + midB, + 0x80 + lowpart) + end +end + +function OBJDEF:onDecodeError(message, text, location, etc) + if text then + if location then + message = string.format("%s at char %d of: %s", message, location, text) + else + message = string.format("%s: %s", message, text) + end + end + if etc ~= nil then + message = message .. " (" .. OBJDEF:encode(etc) .. ")" + end + + if self.assert then + self.assert(false, message) + else + assert(false, message) + end +end + +OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeError +OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError + +function OBJDEF:onEncodeError(message, etc) + if etc ~= nil then + message = message .. " (" .. OBJDEF:encode(etc) .. ")" + end + + if self.assert then + self.assert(false, message) + else + assert(false, message) + end +end + +local function grok_number(self, text, start, etc) + -- + -- Grab the integer part + -- + local integer_part = text:match('^-?[1-9]%d*', start) + or text:match("^-?0", start) + + if not integer_part then + self:onDecodeError("expected number", text, start, etc) + end + + local i = start + integer_part:len() + + -- + -- Grab an optional decimal part + -- + local decimal_part = text:match('^%.%d+', i) or "" + + i = i + decimal_part:len() + + -- + -- Grab an optional exponential part + -- + local exponent_part = text:match('^[eE][-+]?%d+', i) or "" + + i = i + exponent_part:len() + + local full_number_text = integer_part .. decimal_part .. exponent_part + local as_number = tonumber(full_number_text) + + if not as_number then + self:onDecodeError("bad number", text, start, etc) + end + + return as_number, i +end + + +local function grok_string(self, text, start, etc) + + if text:sub(start,start) ~= '"' then + self:onDecodeError("expected string's opening quote", text, start, etc) + end + + local i = start + 1 -- +1 to bypass the initial quote + local text_len = text:len() + local VALUE = "" + while i <= text_len do + local c = text:sub(i,i) + if c == '"' then + return VALUE, i + 1 + end + if c ~= '\\' then + VALUE = VALUE .. c + i = i + 1 + elseif text:match('^\\b', i) then + VALUE = VALUE .. "\b" + i = i + 2 + elseif text:match('^\\f', i) then + VALUE = VALUE .. "\f" + i = i + 2 + elseif text:match('^\\n', i) then + VALUE = VALUE .. "\n" + i = i + 2 + elseif text:match('^\\r', i) then + VALUE = VALUE .. "\r" + i = i + 2 + elseif text:match('^\\t', i) then + VALUE = VALUE .. "\t" + i = i + 2 + else + local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) + if hex then + i = i + 6 -- bypass what we just read + + -- We have a Unicode codepoint. It could be standalone, or if in the proper range and + -- followed by another in a specific range, it'll be a two-code surrogate pair. + local codepoint = tonumber(hex, 16) + if codepoint >= 0xD800 and codepoint <= 0xDBFF then + -- it's a hi surrogate... see whether we have a following low + local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) + if lo_surrogate then + i = i + 6 -- bypass the low surrogate we just read + codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16) + else + -- not a proper low, so we'll just leave the first codepoint as is and spit it out. + end + end + VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint) + + else + + -- just pass through what's escaped + VALUE = VALUE .. text:match('^\\(.)', i) + i = i + 2 + end + end + end + + self:onDecodeError("unclosed string", text, start, etc) +end + +local function skip_whitespace(text, start) + + local match_start, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2 + if match_end then + return match_end + 1 + else + return start + end +end + +local grok_one -- assigned later + +local function grok_object(self, text, start, etc) + if not text:sub(start,start) == '{' then + self:onDecodeError("expected '{'", text, start, etc) + end + + local i = skip_whitespace(text, start + 1) -- +1 to skip the '{' + + local VALUE = self.strictTypes and self:newObject { } or { } + + if text:sub(i,i) == '}' then + return VALUE, i + 1 + end + local text_len = text:len() + while i <= text_len do + local key, new_i = grok_string(self, text, i, etc) + + i = skip_whitespace(text, new_i) + + if text:sub(i, i) ~= ':' then + self:onDecodeError("expected colon", text, i, etc) + end + + i = skip_whitespace(text, i + 1) + + local val, new_i = grok_one(self, text, i) + + VALUE[key] = val + + -- + -- Expect now either '}' to end things, or a ',' to allow us to continue. + -- + i = skip_whitespace(text, new_i) + + local c = text:sub(i,i) + + if c == '}' then + return VALUE, i + 1 + end + + if text:sub(i, i) ~= ',' then + self:onDecodeError("expected comma or '}'", text, i, etc) + end + + i = skip_whitespace(text, i + 1) + end + + self:onDecodeError("unclosed '{'", text, start, etc) +end + +local function grok_array(self, text, start, etc) + if not text:sub(start,start) == '[' then + self:onDecodeError("expected '['", text, start, etc) + end + + local i = skip_whitespace(text, start + 1) -- +1 to skip the '[' + local VALUE = self.strictTypes and self:newArray { } or { } + if text:sub(i,i) == ']' then + return VALUE, i + 1 + end + + local text_len = text:len() + while i <= text_len do + local val, new_i = grok_one(self, text, i) + + table.insert(VALUE, val) + + i = skip_whitespace(text, new_i) + + -- + -- Expect now either ']' to end things, or a ',' to allow us to continue. + -- + local c = text:sub(i,i) + if c == ']' then + return VALUE, i + 1 + end + if text:sub(i, i) ~= ',' then + self:onDecodeError("expected comma or '['", text, i, etc) + end + i = skip_whitespace(text, i + 1) + end + self:onDecodeError("unclosed '['", text, start, etc) +end + + +grok_one = function(self, text, start, etc) + -- Skip any whitespace + start = skip_whitespace(text, start) + + if start > text:len() then + self:onDecodeError("unexpected end of string", text, nil, etc) + end + + if text:find('^"', start) then + return grok_string(self, text, start, etc) + + elseif text:find('^[-0123456789 ]', start) then + return grok_number(self, text, start, etc) + + elseif text:find('^%{', start) then + return grok_object(self, text, start, etc) + + elseif text:find('^%[', start) then + return grok_array(self, text, start, etc) + + elseif text:find('^true', start) then + return true, start + 4 + + elseif text:find('^false', start) then + return false, start + 5 + + elseif text:find('^null', start) then + return nil, start + 4 + + else + self:onDecodeError("can't parse JSON", text, start, etc) + end +end + +function OBJDEF:decode(text, etc) + if type(self) ~= 'table' or self.__index ~= OBJDEF then + OBJDEF:onDecodeError("JSON:decode must be called in method format", nil, nil, etc) + end + + if text == nil then + self:onDecodeOfNilError(string.format("nil passed to JSON:decode()"), nil, nil, etc) + elseif type(text) ~= 'string' then + self:onDecodeError(string.format("expected string argument to JSON:decode(), got %s", type(text)), nil, nil, etc) + end + + if text:match('^%s*$') then + return nil + end + + if text:match('^%s*<') then + -- Can't be JSON... we'll assume it's HTML + self:onDecodeOfHTMLError(string.format("html passed to JSON:decode()"), text, nil, etc) + end + + -- + -- Ensure that it's not UTF-32 or UTF-16. + -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3), + -- but this package can't handle them. + -- + if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then + self:onDecodeError("JSON package groks only UTF-8, sorry", text, nil, etc) + end + + local success, value = pcall(grok_one, self, text, 1, etc) + if success then + return value + else + -- should never get here... JSON parse errors should have been caught earlier + assert(false, value) + return nil + end +end + +local function backslash_replacement_function(c) + if c == "\n" then + return "\\n" + elseif c == "\r" then + return "\\r" + elseif c == "\t" then + return "\\t" + elseif c == "\b" then + return "\\b" + elseif c == "\f" then + return "\\f" + elseif c == '"' then + return '\\"' + elseif c == '\\' then + return '\\\\' + else + return string.format("\\u%04x", c:byte()) + end +end + +local chars_to_be_escaped_in_JSON_string + = '[' + .. '"' -- class sub-pattern to match a double quote + .. '%\\' -- class sub-pattern to match a backslash + .. '%z' -- class sub-pattern to match a null + .. '\001' .. '-' .. '\031' -- class sub-pattern to match control characters + .. ']' + +local function json_string_literal(value) + local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function) + return '"' .. newval .. '"' +end + +local function object_or_array(self, T, etc) + -- + -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON + -- object. If there are only numbers, it's a JSON array. + -- + -- If we'll be converting to a JSON object, we'll want to sort the keys so that the + -- end result is deterministic. + -- + local string_keys = { } + local seen_number_key = false + local maximum_number_key + + for key in pairs(T) do + if type(key) == 'number' then + seen_number_key = true + if not maximum_number_key or maximum_number_key < key then + maximum_number_key = key + end + elseif type(key) == 'string' then + table.insert(string_keys, key) + else + self:onEncodeError("can't encode table with a key of type " .. type(key), etc) + end + end + + if seen_number_key and #string_keys > 0 then + -- + -- Mixed key types... don't know what to do, so bail + -- + self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc) + + elseif #string_keys == 0 then + -- + -- An array + -- + if seen_number_key then + return nil, maximum_number_key -- an array + else + -- + -- An empty table... + -- + if tostring(T) == "JSON array" then + return nil + elseif tostring(T) == "JSON object" then + return { } + else + -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects + return nil + end + end + else + -- + -- An object, so return a list of keys + -- + table.sort(string_keys) + return string_keys + end +end + +-- +-- Encode +-- +local encode_value -- must predeclare because it calls itself +function encode_value(self, value, parents, etc) + + + if value == nil then + return 'null' + end + + if type(value) == 'string' then + return json_string_literal(value) + elseif type(value) == 'number' then + if value ~= value then + -- + -- NaN (Not a Number). + -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option. + -- + return "null" + elseif value >= math.huge then + -- + -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should + -- really be a package option. Note: at least with some implementations, positive infinity + -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is. + -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">=" + -- case first. + -- + return "1e+9999" + elseif value <= -math.huge then + -- + -- Negative infinity. + -- JSON has no INF, so we have to fudge the best we can. This should really be a package option. + -- + return "-1e+9999" + else + return tostring(value) + end + elseif type(value) == 'boolean' then + return tostring(value) + + elseif type(value) ~= 'table' then + self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc) + + else + -- + -- A table to be converted to either a JSON object or array. + -- + local T = value + + if parents[T] then + self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc) + else + parents[T] = true + end + + local result_value + + local object_keys, maximum_number_key = object_or_array(self, T, etc) + if maximum_number_key then + -- + -- An array... + -- + local ITEMS = { } + for i = 1, maximum_number_key do + table.insert(ITEMS, encode_value(self, T[i], parents, etc)) + end + + result_value = "[" .. table.concat(ITEMS, ",") .. "]" + elseif object_keys then + -- + -- An object + -- + + -- + -- We'll always sort the keys, so that comparisons can be made on + -- the results, etc. The actual order is not particularly + -- important (e.g. it doesn't matter what character set we sort + -- as); it's only important that it be deterministic... the same + -- every time. + -- + local PARTS = { } + for _, key in ipairs(object_keys) do + local encoded_key = encode_value(self, tostring(key), parents, etc) + local encoded_val = encode_value(self, T[key], parents, etc) + table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val)) + end + result_value = "{" .. table.concat(PARTS, ",") .. "}" + else + -- + -- An empty array/object... we'll treat it as an array, though it should really be an option + -- + result_value = "[]" + end + + parents[T] = false + return result_value + end +end + +local encode_pretty_value -- must predeclare because it calls itself +function encode_pretty_value(self, value, parents, indent, etc) + + if type(value) == 'string' then + return json_string_literal(value) + + elseif type(value) == 'number' then + return tostring(value) + + elseif type(value) == 'boolean' then + return tostring(value) + + elseif type(value) == 'nil' then + return 'null' + + elseif type(value) ~= 'table' then + self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc) + + else + -- + -- A table to be converted to either a JSON object or array. + -- + local T = value + + if parents[T] then + self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc) + end + parents[T] = true + + local result_value + + local object_keys = object_or_array(self, T, etc) + if not object_keys then + -- + -- An array... + -- + local ITEMS = { } + for i = 1, #T do + table.insert(ITEMS, encode_pretty_value(self, T[i], parents, indent, etc)) + end + + result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]" + + else + + -- + -- An object -- can keys be numbers? + -- + + local KEYS = { } + local max_key_length = 0 + for _, key in ipairs(object_keys) do + local encoded = encode_pretty_value(self, tostring(key), parents, "", etc) + max_key_length = math.max(max_key_length, #encoded) + table.insert(KEYS, encoded) + end + local key_indent = indent .. " " + local subtable_indent = indent .. string.rep(" ", max_key_length + 2 + 4) + local FORMAT = "%s%" .. tostring(max_key_length) .. "s: %s" + + local COMBINED_PARTS = { } + for i, key in ipairs(object_keys) do + local encoded_val = encode_pretty_value(self, T[key], parents, subtable_indent, etc) + table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val)) + end + result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}" + end + + parents[T] = false + return result_value + end +end + +function OBJDEF:encode(value, etc) + if type(self) ~= 'table' or self.__index ~= OBJDEF then + OBJDEF:onEncodeError("JSON:encode must be called in method format", etc) + end + + local parents = {} + return encode_value(self, value, parents, etc) +end + +function OBJDEF:encode_pretty(value, etc) + local parents = {} + local subtable_indent = "" + return encode_pretty_value(self, value, parents, subtable_indent, etc) +end + +function OBJDEF.__tostring() + return "JSON encode/decode package" +end + +OBJDEF.__index = OBJDEF + +function OBJDEF:new(args) + local new = { } + + if args then + for key, val in pairs(args) do + new[key] = val + end + end + + return setmetatable(new, OBJDEF) +end + +return OBJDEF:new() + +-- +-- Version history: +-- +-- 20111207.5 Added support for the 'etc' arguments, for better error reporting. +-- +-- 20110731.4 More feedback from David Kolf on how to make the tests for Nan/Infinity system independent. +-- +-- 20110730.3 Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules: +-- +-- * When encoding lua for JSON, Sparse numeric arrays are now handled by +-- spitting out full arrays, such that +-- JSON:encode({"one", "two", [10] = "ten"}) +-- returns +-- ["one","two",null,null,null,null,null,null,null,"ten"] +-- +-- In 20100810.2 and earlier, only up to the first non-null value would have been retained. +-- +-- * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999". +-- Version 20100810.2 and earlier created invalid JSON in both cases. +-- +-- * Unicode surrogate pairs are now detected when decoding JSON. +-- +-- 20100810.2 added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding +-- +-- 20100731.1 initial public release +-- diff --git a/frontend/document/djvudocument.lua b/frontend/document/djvudocument.lua index 5d7808f8a..2c386a3be 100644 --- a/frontend/document/djvudocument.lua +++ b/frontend/document/djvudocument.lua @@ -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 = {} diff --git a/frontend/document/document.lua b/frontend/document/document.lua index 77e3f035b..5e53ecb8f 100644 --- a/frontend/document/document.lua +++ b/frontend/document/document.lua @@ -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) diff --git a/frontend/document/koptinterface.lua b/frontend/document/koptinterface.lua index 88f40ddd7..e1af67ef7 100644 --- a/frontend/document/koptinterface.lua +++ b/frontend/document/koptinterface.lua @@ -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 --- 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: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 cached koptcontext for centain page. if context doesn't exist in cache make --- new context and reflow the src page immediatly. +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, 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 diff --git a/frontend/document/pdfdocument.lua b/frontend/document/pdfdocument.lua index c5fe50c7d..78f633b1a 100644 --- a/frontend/document/pdfdocument.lua +++ b/frontend/document/pdfdocument.lua @@ -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) diff --git a/frontend/ui/reader/readerdictionary.lua b/frontend/ui/reader/readerdictionary.lua new file mode 100644 index 000000000..bc5e1a973 --- /dev/null +++ b/frontend/ui/reader/readerdictionary.lua @@ -0,0 +1,55 @@ +require "ui/device" +require "ui/widget/dict" + +ReaderDictionary = EventListener:new{} + +function ReaderDictionary:init() + local dev_mod = Device:getModel() + if dev_mod == "KindlePaperWhite" or dev_mod == "KindleTouch" then + require("liblipclua") + JSON = require("JSON") + self.lipc_handle = lipc.init("com.github.koreader.dictionary") + end +end + +function ReaderDictionary:onLookupWord(word) + DEBUG("lookup word:", word) + --self:quickLookup() + if self.lipc_handle and JSON and word then + self.lipc_handle:set_string_property( + "com.lab126.booklet.kpvbooklet.dict", "lookup", word) + local results_str = self.lipc_handle:get_string_property( + "com.lab126.booklet.kpvbooklet.word", word) + if results_str then + --DEBUG("def str:", word, definitions) + local ok, results_tab = pcall(JSON.decode, JSON, results_str) + --DEBUG("lookup result table:", word, results_tab) + if results_tab[1] then + self:quickLookup(results_tab[1]) + end + end + end + return true +end + +function ReaderDictionary:quickLookup(result) +-- UIManager:show(DictQuickLookup:new{ +-- dict = "Oxford Dictionary of English", +-- definition = "coordination n. [mass noun] 1 the organization of the different elements of a \ +-- complex body or activity so as to enable them to work together effectively: an important managerial \ +-- task is the control and coordination of activities. cooperative effort resulting in an effective \ +-- relationship: action groups work in coordination with local groups to end rainforest destruction. \ +-- the ability to use different parts of the body together smoothly and efficiently: changing from \ +-- one foot position to another requires coordination and balance.", +-- id = "/mnt/us/documents/dictionaries/Oxford_Dictionary_of_English.azw", +-- lang = "en", +-- }) + if result then + UIManager:show(DictQuickLookup:new{ + dict = result.dict, + definition = result.definition, + id = result.ID, + lang = result.lang, + }) + end +end diff --git a/frontend/ui/reader/readerhighlight.lua b/frontend/ui/reader/readerhighlight.lua new file mode 100644 index 000000000..76fb3256d --- /dev/null +++ b/frontend/ui/reader/readerhighlight.lua @@ -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 diff --git a/frontend/ui/reader/readerkopt.lua b/frontend/ui/reader/readerkopt.lua index 74582c762..479f65fda 100644 --- a/frontend/ui/reader/readerkopt.lua +++ b/frontend/ui/reader/readerkopt.lua @@ -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 + diff --git a/frontend/ui/reader/readerview.lua b/frontend/ui/reader/readerview.lua index 641e56108..d92d6c350 100644 --- a/frontend/ui/reader/readerview.lua +++ b/frontend/ui/reader/readerview.lua @@ -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, @@ -96,6 +100,11 @@ function ReaderView:paintTo(bb, x, y) self.dim_area.w, self.dim_area.h ) end + + -- draw highlight + if self.highlight.rect then + self:drawHightlight(bb, x, y, self.highlight.rect) + end -- paint dogear if self.dogear_visible then @@ -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) diff --git a/frontend/ui/readerui.lua b/frontend/ui/readerui.lua index a041893fa..d4eb54025 100644 --- a/frontend/ui/readerui.lua +++ b/frontend/ui/readerui.lua @@ -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, diff --git a/frontend/ui/widget/dict.lua b/frontend/ui/widget/dict.lua new file mode 100644 index 000000000..5716bdeda --- /dev/null +++ b/frontend/ui/widget/dict.lua @@ -0,0 +1,69 @@ +require "ui/widget/container" + +--[[ +Display quick lookup word definition +]] +DictQuickLookup = InputContainer:new{ + dict = nil, + definition = nil, + id = nil, + lang = nil, + + title_face = Font:getFace("tfont", 20), + content_face = Font:getFace("cfont", 18), + width = Screen:getWidth() - 100 +} + +function DictQuickLookup:init() + if Device:hasKeyboard() then + key_events = { + AnyKeyPressed = { { Input.group.Any }, + seqtext = "any key", doc = _("close dialog") } + } + else + self.ges_events.TapClose = { + GestureRange:new{ + ges = "tap", + range = Geom:new{ + x = 0, y = 0, + w = Screen:getWidth(), + h = Screen:getHeight(), + } + } + } + end + -- we construct the actual content here because self.text is only available now + self[1] = CenterContainer:new{ + dimen = Screen:getSize(), + FrameContainer:new{ + margin = 2, + background = 0, + VerticalGroup:new{ + align = "center", + -- title bar + TextBoxWidget:new{ + text = self.dict, + face = self.title_face, + width = self.width, + }, + VerticalSpan:new{ width = 20 }, + TextBoxWidget:new{ + text = self.definition, + face = self.content_face, + width = self.width, + } + } + } + } +end + +function DictQuickLookup:onAnyKeyPressed() + -- triggered by our defined key events + UIManager:close(self) + return true +end + +function DictQuickLookup:onTapClose() + UIManager:close(self) + return true +end diff --git a/koreader-base b/koreader-base index 8f26cc326..3726a3a3a 160000 --- a/koreader-base +++ b/koreader-base @@ -1 +1 @@ -Subproject commit 8f26cc3264f21006b5079030381bd593deb9c9e0 +Subproject commit 3726a3a3aaa590edf6ffc15f4a59e587c9253bce diff --git a/koreader.sh b/koreader.sh index a7348b2d7..e364de239 100755 --- a/koreader.sh +++ b/koreader.sh @@ -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