diff --git a/.gitignore b/.gitignore index 8a050081b..21f649e41 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,13 @@ lua lua-* .reader.kpdfview.lua mupdf-thirdparty.zip +djvulibre* kpdfview *.o +kindlepdfviewer-*.zip /.cproject /.project /.reader.kpdfview + diff --git a/blitbuffer.c b/blitbuffer.c index 93642fd61..2294b8d19 100644 --- a/blitbuffer.c +++ b/blitbuffer.c @@ -366,6 +366,63 @@ static int paintRect(lua_State *L) { return 0; } +static int invertRect(lua_State *L) { + BlitBuffer *dst = (BlitBuffer*) luaL_checkudata(L, 1, "blitbuffer"); + int x = luaL_checkint(L, 2); + int y = luaL_checkint(L, 3); + int w = luaL_checkint(L, 4); + int h = luaL_checkint(L, 5); + uint8_t *dstptr; + + int cy, cx; + if(w <= 0 || h <= 0 || x >= dst->w || y >= dst->h) { + return 0; + } + if(x + w > dst->w) { + w = dst->w - x; + } + if(y + h > dst->h) { + h = dst->h - y; + } + + if(x & 1) { + /* This will invert the leftmost column + * in the case when x is odd. After this, + * x will become even. */ + dstptr = (uint8_t*)(dst->data + + y * dst->pitch + + x / 2); + for(cy = 0; cy < h; cy++) { + *dstptr ^= 0x0F; + dstptr += dst->pitch; + } + x++; + w--; + } + dstptr = (uint8_t*)(dst->data + + y * dst->pitch + + x / 2); + for(cy = 0; cy < h; cy++) { + for(cx = 0; cx < w/2; cx++) { + *(dstptr+cx) ^= 0xFF; + } + dstptr += dst->pitch; + } + if(w & 1) { + /* This will invert the rightmost column + * in the case when (w & 1) && !(x & 1) or + * !(w & 1) && (x & 1). */ + dstptr = (uint8_t*)(dst->data + + y * dst->pitch + + (x + w) / 2); + for(cy = 0; cy < h; cy++) { + *dstptr ^= 0xF0; + dstptr += dst->pitch; + } + } + return 0; +} + static const struct luaL_Reg blitbuffer_func[] = { {"new", newBlitBuffer}, {NULL, NULL} @@ -378,6 +435,7 @@ static const struct luaL_Reg blitbuffer_meth[] = { {"addblitFrom", addblitToBuffer}, {"blitFullFrom", blitFullToBuffer}, {"paintRect", paintRect}, + {"invertRect", invertRect}, {"free", freeBlitBuffer}, {"__gc", freeBlitBuffer}, {NULL, NULL} diff --git a/djvu.c b/djvu.c index 090d625a3..6bcac2290 100644 --- a/djvu.c +++ b/djvu.c @@ -206,7 +206,7 @@ static int openPage(lua_State *L) { static int getPageSize(lua_State *L) { DjvuPage *page = (DjvuPage*) luaL_checkudata(L, 1, "djvupage"); DrawContext *dc = (DrawContext*) luaL_checkudata(L, 2, "drawcontext"); - + lua_pushnumber(L, dc->zoom * page->info.width); lua_pushnumber(L, dc->zoom * page->info.height); @@ -225,6 +225,128 @@ static int getUsedBBox(lua_State *L) { return 4; } + +/* + * Return a table like following: + * { + * -- a line entry + * 1 = { + * 1 = {word="This", x0=377, y0=4857, x1=2427, y1=5089}, + * 2 = {word="is", x0=377, y0=4857, x1=2427, y1=5089}, + * 3 = {word="Word", x0=377, y0=4857, x1=2427, y1=5089}, + * 4 = {word="List", x0=377, y0=4857, x1=2427, y1=5089}, + * x0 = 377, y0 = 4857, x1 = 2427, y1 = 5089, + * }, + * + * -- an other line entry + * 2 = { + * 1 = {word="This", x0=377, y0=4857, x1=2427, y1=5089}, + * 2 = {word="is", x0=377, y0=4857, x1=2427, y1=5089}, + * x0 = 377, y0 = 4857, x1 = 2427, y1 = 5089, + * }, + * } + */ +static int getPageText(lua_State *L) { + DjvuDocument *doc = (DjvuDocument*) luaL_checkudata(L, 1, "djvudocument"); + int pageno = luaL_checkint(L, 2); + + miniexp_t sexp, se_line, se_word; + int i = 1, j = 1, counter_l = 1, counter_w=1, + nr_line = 0, nr_word = 0; + const char *word = NULL; + + while ((sexp = ddjvu_document_get_pagetext(doc->doc_ref, pageno-1, "word")) + == miniexp_dummy) { + handle(L, doc->context, True); + } + + /* throuw page info and obtain lines info, after this, sexp's entries + * are lines. */ + sexp = miniexp_cdr(sexp); + /* get number of lines in a page */ + nr_line = miniexp_length(sexp); + /* table that contains all the lines */ + lua_newtable(L); + + counter_l = 1; + for(i = 1; i <= nr_line; i++) { + /* retrive one line entry */ + se_line = miniexp_nth(i, sexp); + nr_word = miniexp_length(se_line); + if(nr_word == 0) { + continue; + } + + /* subtable that contains words in a line */ + lua_pushnumber(L, counter_l); + lua_newtable(L); + counter_l++; + + /* set line position */ + lua_pushstring(L, "x0"); + lua_pushnumber(L, miniexp_to_int(miniexp_nth(1, se_line))); + lua_settable(L, -3); + + lua_pushstring(L, "y0"); + lua_pushnumber(L, miniexp_to_int(miniexp_nth(2, se_line))); + lua_settable(L, -3); + + lua_pushstring(L, "x1"); + lua_pushnumber(L, miniexp_to_int(miniexp_nth(3, se_line))); + lua_settable(L, -3); + + lua_pushstring(L, "y1"); + lua_pushnumber(L, miniexp_to_int(miniexp_nth(4, se_line))); + lua_settable(L, -3); + + /* now loop through each word in the line */ + counter_w = 1; + for(j = 1; j <= nr_word; j++) { + /* retrive one word entry */ + se_word = miniexp_nth(j, se_line); + /* check to see whether the entry is empty */ + word = miniexp_to_str(miniexp_nth(5, se_word)); + if (!word) { + continue; + } + + /* create table that contains info for a word */ + lua_pushnumber(L, counter_w); + lua_newtable(L); + counter_w++; + + /* set word info */ + lua_pushstring(L, "x0"); + lua_pushnumber(L, miniexp_to_int(miniexp_nth(1, se_word))); + lua_settable(L, -3); + + lua_pushstring(L, "y0"); + lua_pushnumber(L, miniexp_to_int(miniexp_nth(2, se_word))); + lua_settable(L, -3); + + lua_pushstring(L, "x1"); + lua_pushnumber(L, miniexp_to_int(miniexp_nth(3, se_word))); + lua_settable(L, -3); + + lua_pushstring(L, "y1"); + lua_pushnumber(L, miniexp_to_int(miniexp_nth(4, se_word))); + lua_settable(L, -3); + + lua_pushstring(L, "word"); + lua_pushstring(L, word); + lua_settable(L, -3); + + /* set word entry to line subtable */ + lua_settable(L, -3); + } /* end of for (j) */ + + /* set line entry to page text table */ + lua_settable(L, -3); + } /* end of for (i) */ + + return 1; +} + static int closePage(lua_State *L) { DjvuPage *page = (DjvuPage*) luaL_checkudata(L, 1, "djvupage"); if(page->page_ref != NULL) { @@ -334,6 +456,7 @@ static const struct luaL_Reg djvudocument_meth[] = { {"openPage", openPage}, {"getPages", getNumberOfPages}, {"getTOC", getTableOfContent}, + {"getPageText", getPageText}, {"close", closeDocument}, {"__gc", closeDocument}, {NULL, NULL} diff --git a/djvureader.lua b/djvureader.lua index 0582073b9..cd0ad9885 100644 --- a/djvureader.lua +++ b/djvureader.lua @@ -12,3 +12,608 @@ function DJVUReader:open(filename) end return ok end + + + +-----------[ highlight support ]---------- + +---------------------------------------------------- +-- Given coordinates of four conners and return +-- coordinate of upper left conner with with and height +-- +-- In djvulibre library, some coordinates starts from +-- down left conner, i.e. y is upside down. This method +-- only transform these coordinates. +---------------------------------------------------- +function DJVUReader:_rectCoordTransform(x0, y0, x1, y1) + return + self.offset_x + x0 * self.globalzoom, + self.offset_y + self.cur_full_height - (y1 * self.globalzoom), + (x1 - x0) * self.globalzoom, + (y1 - y0) * self.globalzoom +end + +-- make sure the whole word can be seen in screen +function DJVUReader:_isEntireWordInScreenRange(w) + return self:_isEntireWordInScreenHeightRange(w) and + self:_isEntireWordInScreenWidthRange(w) +end + +-- y axel in djvulibre starts from bottom +function DJVUReader:_isEntireWordInScreenHeightRange(w) + return (w ~= nil) and + (self.cur_full_height - (w.y1 * self.globalzoom) >= + -self.offset_y) and + (self.cur_full_height - (w.y0 * self.globalzoom) <= + -self.offset_y + height) +end + +function DJVUReader:_isEntireWordInScreenWidthRange(w) + return (w ~= nil) and + (w.x0 * self.globalzoom >= -self.offset_x) and + (w.x1 * self.globalzoom <= -self.offset_x + width) +end + +-- make sure at least part of the word can be seen in screen +function DJVUReader:_isWordInScreenRange(w) + return (w ~= nil) and + (self.cur_full_height - (w.y0 * self.globalzoom) >= + -self.offset_y) and + (self.cur_full_height - (w.y1 * self.globalzoom) <= + -self.offset_y + height) and + (w.x1 * self.globalzoom >= -self.offset_x) and + (w.x0 * self.globalzoom <= -self.offset_x + width) +end + +function DJVUReader:toggleTextHighLight(word_list) + for _,text_item in ipairs(word_list) do + for _,line_item in ipairs(text_item) do + -- make sure that line is in screen range + if self:_isEntireWordInScreenHeightRange(line_item) then + local x, y, w, h = self:_rectCoordTransform( + line_item.x0, line_item.y0, + line_item.x1, line_item.y1) + -- slightly enlarge the highlight height + -- for better viewing experience + x = x + y = y - h * 0.1 + w = w + h = h * 1.2 + + 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 + fb.bb:paintRect(x, y+h-1, w, + self.highlight.line_width, + self.highlight.line_color) + elseif self.highlight.drawer == "marker" then + fb.bb:invertRect(x, y, w, h) + end + end -- EOF if isEntireWordInScreenHeightRange + end -- EOF for line_item + end -- EOF for text_item +end + +function DJVUReader:_wordIterFromRange(t, l0, w0, l1, w1) + local i = l0 + local j = w0 - 1 + return function() + if i <= l1 then + -- if in line range, loop through lines + if i == l1 then + -- in last line + if j < w1 then + j = j + 1 + else + -- out of range return nil + return nil, nil + end + else + if j < #t[i] then + j = j + 1 + else + -- goto next line + i = i + 1 + j = 1 + end + end + return i, j + end + end -- EOF closure +end + +function DJVUReader:_toggleWordHighLight(t, l, w) + x, y, w, h = self:_rectCoordTransform(t[l][w].x0, t[l].y0, + t[l][w].x1, t[l].y1) + -- slightly enlarge the highlight range for better viewing experience + x = x - w * 0.05 + y = y - h * 0.05 + w = w * 1.1 + h = h * 1.1 + + fb.bb:invertRect(x, y, w, h) +end + +function DJVUReader:_toggleTextHighLight(t, l0, w0, l1, w1) + --print("# toggle range", l0, w0, l1, w1) + -- make sure (l0, w0) is smaller than (l1, w1) + if l0 > l1 then + l0, l1 = l1, l0 + w0, w1 = w1, w0 + elseif l0 == l1 and w0 > w1 then + w0, w1 = w1, w0 + end + + for _l, _w in self:_wordIterFromRange(t, l0, w0, l1, w1) do + if self:_isWordInScreenRange(t[_l][_w]) then + -- blitbuffer module will take care of the out of screen range part. + self:_toggleWordHighLight(t, _l, _w) + end + end +end + +-- remember to clear cursor before calling this +function DJVUReader:drawCursorAfterWord(t, l, w) + self.cursor:setHeight((t[l].y1 - t[l].y0) * self.globalzoom) + self.cursor:moveTo( + self.offset_x + t[l][w].x1 * self.globalzoom, + self.offset_y + self.cur_full_height - (t[l].y1 * self.globalzoom)) + self.cursor:draw() +end + +function DJVUReader:drawCursorBeforeWord(t, l, w) + self.cursor:setHeight((t[l].y1 - t[l].y0) + * self.globalzoom) + self.cursor:moveTo( + self.offset_x + t[l][w].x0 * self.globalzoom - self.cursor.w, + self.offset_y + self.cur_full_height - t[l].y1 * self.globalzoom) + self.cursor:draw() +end + +function DJVUReader:startHighLightMode() + local t = self.doc:getPageText(self.pageno) + + local function _findFirstWordInView(t) + for i=1, #t, 1 do + if self:_isEntireWordInScreenRange(t[i][1]) then + return i, 1 + end + end + + return nil + end + + local function _prevWord(t, cur_l, cur_w) + if cur_l == 1 then + if cur_w == 1 then + -- already the first word + return 1, 1 + else + -- in first line, but not first word + return cur_l, cur_w -1 + end + end + + if cur_w <= 1 then + -- first word in current line, goto previous line + return cur_l - 1, #t[cur_l-1] + else + return cur_l, cur_w - 1 + end + end + + local function _nextWord(t, cur_l, cur_w) + if cur_l == #t then + if cur_w == #(t[cur_l]) then + -- already the last word + return cur_l, cur_w + else + -- in last line, but not last word + return cur_l, cur_w + 1 + end + end + + if cur_w < #t[cur_l] then + return cur_l, cur_w + 1 + else + -- last word in current line, move to next line + return cur_l + 1, 1 + end + end + + local function _wordInNextLine(t, cur_l, cur_w) + if cur_l == #t then + -- already in last line, return the last word + return cur_l, #(t[cur_l]) + else + return cur_l + 1, math.min(cur_w, #t[cur_l+1]) + end + end + + local function _wordInPrevLine(t, cur_l, cur_w) + if cur_l == 1 then + -- already in first line, return the first word + return 1, 1 + else + return cur_l - 1, math.min(cur_w, #t[cur_l-1]) + end + end + + local function _isMovingForward(l, w) + return l.cur > l.start or (l.cur == l.start and w.cur > w.start) + end + + local l = {} + local w = {} + + l.start, w.start = _findFirstWordInView(t) + if not l.start then + print("# no text in current view!") + return + end + + l.cur, w.cur = l.start, w.start + l.new, w.new = l.cur, w.cur + local is_meet_start = false + local is_meet_end = false + local running = true + + self.cursor = Cursor:new { + x_pos = t[l.cur][w.cur].x1*self.globalzoom, + y_pos = self.offset_y + (self.cur_full_height + - (t[l.cur][w.cur].y1 * self.globalzoom)), + h = (t[l.cur][w.cur].y1 - t[l.cur][w.cur].y0) * self.globalzoom, + line_width_factor = 4, + } + self.cursor:draw() + fb:refresh(1) + + -- first use cursor to place start pos for highlight + while running do + local ev = input.waitForEvent() + ev.code = adjustKeyEvents(ev) + if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then + if ev.code == KEY_FW_LEFT then + if w.cur == 1 then + w.cur = 0 + w.new = 0 + else + if w.cur == 0 then + -- already at the left end of current line, + -- goto previous line (_prevWord does not understand + -- zero w.cur) + w.cur = 1 + end + l.new, w.new = _prevWord(t, l.cur, w.cur) + end + + self.cursor:clear() + if w.new ~= 0 + and not self:_isEntireWordInScreenHeightRange(t[l.new][w.new]) + and self:_isEntireWordInScreenWidthRange(t[l.new][w.new]) then + -- word is in previous view + local pageno = self:prevView() + self:goto(pageno) + end + + -- update cursor + if w.cur == 0 then + -- meet line left end, must be handled as special case + if self:_isEntireWordInScreenRange(t[l.cur][1]) then + self:drawCursorBeforeWord(t, l.cur, 1) + end + else + if self:_isEntireWordInScreenRange(t[l.new][w.new]) then + self:drawCursorAfterWord(t, l.new, w.new) + end + end + elseif ev.code == KEY_FW_RIGHT then + if w.cur == 0 then + w.cur = 1 + w.new = 1 + else + l.new, w.new = _nextWord(t, l.cur, w.cur) + if w.new == 1 then + -- Must be come from the right end of previous line, + -- so goto the left end of current line. + w.cur = 0 + w.new = 0 + end + end + + self.cursor:clear() + + local tmp_w = w.new + if w.cur == 0 then + tmp_w = 1 + end + if not self:_isEntireWordInScreenHeightRange(t[l.new][tmp_w]) + and self:_isEntireWordInScreenWidthRange(t[l.new][tmp_w]) then + local pageno = self:nextView() + self:goto(pageno) + end + + if w.cur == 0 then + -- meet line left end, must be handled as special case + if self:_isEntireWordInScreenRange(t[l.new][1]) then + self:drawCursorBeforeWord(t, l.new, 1) + end + else + if self:_isEntireWordInScreenRange(t[l.new][w.new]) then + self:drawCursorAfterWord(t, l.new, w.new) + end + end + elseif ev.code == KEY_FW_UP then + if w.cur == 0 then + -- goto left end of last line + l.new = math.max(l.cur - 1, 1) + elseif l.cur == 1 and w.cur == 1 then + -- already first word, to the left end of first line + w.new = 0 + else + l.new, w.new = _wordInPrevLine(t, l.cur, w.cur) + end + + self.cursor:clear() + + local tmp_w = w.new + if w.cur == 0 then + tmp_w = 1 + end + if not self:_isEntireWordInScreenHeightRange(t[l.new][tmp_w]) + and self:_isEntireWordInScreenWidthRange(t[l.new][tmp_w]) then + -- goto next view of current page + local pageno = self:prevView() + self:goto(pageno) + end + + if w.new == 0 then + if self:_isEntireWordInScreenRange(t[l.new][1]) then + self:drawCursorBeforeWord(t, l.new, 1) + end + else + if self:_isEntireWordInScreenRange(t[l.new][w.new]) then + self:drawCursorAfterWord(t, l.new, w.new) + end + end + elseif ev.code == KEY_FW_DOWN then + if w.cur == 0 then + -- on the left end of current line, + -- goto left end of next line + l.new = math.min(l.cur + 1, #t) + else + l.new, w.new = _wordInNextLine(t, l.cur, w.cur) + end + + self.cursor:clear() + + local tmp_w = w.new + if w.cur == 0 then + tmp_w = 1 + end + if not self:_isEntireWordInScreenHeightRange(t[l.new][tmp_w]) + and self:_isEntireWordInScreenWidthRange(t[l.new][tmp_w]) then + -- goto next view of current page + local pageno = self:nextView() + self:goto(pageno) + end + + if w.cur == 0 then + if self:_isEntireWordInScreenRange(t[l.new][1]) then + self:drawCursorBeforeWord(t, l.new, 1) + end + else + if self:_isEntireWordInScreenRange(t[l.new][w.new]) then + self:drawCursorAfterWord(t, l.new, w.new) + end + end + elseif ev.code == KEY_DEL then + if self.highlight[self.pageno] then + for k, text_item in ipairs(self.highlight[self.pageno]) do + for _, line_item in ipairs(text_item) do + if t[l.cur][w.cur].y0 >= line_item.y0 + and t[l.cur][w.cur].y1 <= line_item.y1 + and t[l.cur][w.cur].x0 >= line_item.x0 + and t[l.cur][w.cur].x1 <= line_item.x1 then + self.highlight[self.pageno][k] = nil + end + end -- EOF for line_item + end -- EOF for text_item + end -- EOF if not highlight table + if #self.highlight[self.pageno] == 0 then + self.highlight[self.pageno] = nil + end + return + elseif ev.code == KEY_FW_PRESS then + if w.cur == 0 then + w.cur = 1 + l.cur, w.cur = _prevWord(t, l.cur, w.cur) + end + l.new, w.new = l.cur, w.cur + l.start, w.start = l.cur, w.cur + running = false + self.cursor:clear() + elseif ev.code == KEY_BACK then + running = false + return + end -- EOF if key event + l.cur, w.cur = l.new, w.new + fb:refresh(1) + end + end -- EOF while + --print("start", l.cur, w.cur, l.start, w.start) + + -- two helper functions for highlight + local function _togglePrevWordHighLight(t, l, w) + l.new, w.new = _prevWord(t, l.cur, w.cur) + + if l.cur == 1 and w.cur == 1 then + is_meet_start = true + -- left end of first line must be handled as special case + w.new = 0 + end + + if w.new ~= 0 and + not self:_isEntireWordInScreenHeightRange(t[l.new][w.new]) then + -- word out of left and right sides of current view should + -- not trigger pan by page + if self:_isEntireWordInScreenWidthRange(t[l.new][w.new]) then + -- word is in previous view + local pageno = self:prevView() + self:goto(pageno) + end + + local l0 = l.start + local w0 = w.start + local l1 = l.cur + local w1 = w.cur + if _isMovingForward(l, w) then + l0, w0 = _nextWord(t, l0, w0) + l1, w1 = l.new, w.new + end + self:_toggleTextHighLight(t, l0, w0, + l1, w1) + else + self:_toggleWordHighLight(t, l.cur, w.cur) + end + + l.cur, w.cur = l.new, w.new + return l, w, (is_meet_start or false) + end + + local function _toggleNextWordHighLight(t, l, w) + if w.cur == 0 then + w.new = 1 + else + l.new, w.new = _nextWord(t, l.cur, w.cur) + end + if l.new == #t and w.new == #t[#t] then + is_meet_end = true + end + + if not self:_isEntireWordInScreenHeightRange(t[l.new][w.new]) then + if self:_isEntireWordInScreenWidthRange(t[l.new][w.new]) then + local pageno = self:nextView() + self:goto(pageno) + end + + local tmp_l = l.start + local tmp_w = w.start + if _isMovingForward(l, w) then + tmp_l, tmp_w = _nextWord(t, tmp_l, tmp_w) + end + self:_toggleTextHighLight(t, tmp_l, tmp_w, + l.new, w.new) + else + self:_toggleWordHighLight(t, l.new, w.new) + end + + l.cur, w.cur = l.new, w.new + return l, w, (is_meet_end or false) + end + + + -- go into highlight mode + running = true + while running do + local ev = input.waitForEvent() + ev.code = adjustKeyEvents(ev) + if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then + if ev.code == KEY_FW_LEFT then + is_meet_end = false + if not is_meet_start then + l, w, is_meet_start = _togglePrevWordHighLight(t, l, w) + end + elseif ev.code == KEY_FW_RIGHT then + is_meet_start = false + if not is_meet_end then + l, w, is_meet_end = _toggleNextWordHighLight(t, l, w) + end -- EOF if is not is_meet_end + elseif ev.code == KEY_FW_UP then + is_meet_end = false + if not is_meet_start then + if l.cur == 1 then + -- handle left end of first line as special case + tmp_l = 1 + tmp_w = 0 + else + tmp_l, tmp_w = _wordInPrevLine(t, l.cur, w.cur) + end + while not (tmp_l == l.cur and tmp_w == w.cur) do + l, w, is_meet_start = _togglePrevWordHighLight(t, l, w) + end + end + elseif ev.code == KEY_FW_DOWN then + is_meet_start = false + if not is_meet_end then + if w.cur == 0 then + -- handle left end of first line as special case + tmp_l = math.min(tmp_l + 1, #t) + tmp_w = 1 + else + tmp_l, tmp_w = _wordInNextLine(t, l.new, w.new) + end + while not (tmp_l == l.cur and tmp_w == w.cur) do + l, w, is_meet_end = _toggleNextWordHighLight(t, l, w) + end + end + elseif ev.code == KEY_FW_PRESS then + local l0, w0, l1, w1 + + -- find start and end of highlight text + if _isMovingForward(l, w) then + l0, w0 = _nextWord(t, l.start, w.start) + l1, w1 = l.cur, w.cur + else + l0, w0 = _nextWord(t, l.cur, w.cur) + l1, w1 = l.start, w.start + end + -- remove selection area + self:_toggleTextHighLight(t, l0, w0, l1, w1) + + -- put text into highlight table of current page + local hl_item = {} + local s = "" + local prev_l = l0 + local prev_w = w0 + local l_item = { + x0 = t[l0][w0].x0, + y0 = t[l0].y0, + y1 = t[l0].y1, + } + for _l,_w in self:_wordIterFromRange(t, l0, w0, l1, w1) do + local word_item = t[_l][_w] + if _l > prev_l then + -- in next line, add previous line to highlight item + l_item.x1 = t[prev_l][prev_w].x1 + table.insert(hl_item, l_item) + -- re initialize l_item for new line + l_item = { + x0 = word_item.x0, + y0 = t[_l].y0, + y1 = t[_l].y1, + } + end + s = s .. word_item.word .. " " + prev_l, prev_w = _l, _w + end + -- insert last line of text in line item + l_item.x1 = t[prev_l][prev_w].x1 + table.insert(hl_item, l_item) + hl_item.text = s + + if not self.highlight[self.pageno] then + self.highlight[self.pageno] = {} + end + table.insert(self.highlight[self.pageno], hl_item) + + running = false + elseif ev.code == KEY_BACK then + running = false + end -- EOF if key event + fb:refresh(1) + end + end -- EOF while +end + diff --git a/filechooser.lua b/filechooser.lua index e1be925cb..58c294f65 100644 --- a/filechooser.lua +++ b/filechooser.lua @@ -197,7 +197,7 @@ function FileChooser:choose(ypos, height) end end pagedirty = true - elseif ev.code == KEY_PGFWD then + elseif ev.code == KEY_PGFWD or ev.code == KEY_LPGFWD then if self.page < (self.items / perpage) then if self.current + self.page*perpage > self.items then self.current = self.items - self.page*perpage @@ -208,7 +208,7 @@ function FileChooser:choose(ypos, height) self.current = self.items - (self.page-1)*perpage markerdirty = true end - elseif ev.code == KEY_PGBCK then + elseif ev.code == KEY_PGBCK or ev.code == KEY_LPGBCK then if self.page > 1 then self.page = self.page - 1 pagedirty = true diff --git a/graphics.lua b/graphics.lua index 1ebb13cce..267cb9469 100644 --- a/graphics.lua +++ b/graphics.lua @@ -6,6 +6,7 @@ blitbuffer.paintBorder = function (bb, x, y, w, h, bw, c) bb:paintRect(x+w-bw, y+bw, bw, h - 2*bw, c) end + --[[ Draw a progress bar according to following args: @@ -27,3 +28,108 @@ blitbuffer.progressBar = function (bb, x, y, w, h, fb.bb:paintRect(x+load_m_w, y+load_m_h, (w-2*load_m_w)*load_percent, (h-2*load_m_h), c) end + + + +------------------------------------------------ +-- Start of Cursor class +------------------------------------------------ + +Cursor = { + x_pos = 0, + y_pos = 0, + --color = 15, + h = 10, + w = nil, + line_w = nil, + is_cleared = true, +} + +function Cursor:new(o) + o = o or {} + o.x_pos = o.x_pos or self.x_pos + o.y_pos = o.y_pos or self.y_pos + o.line_width_factor = o.line_width_factor or 10 + + setmetatable(o, self) + self.__index = self + + o:setHeight(o.h or self.h) + return o +end + +function Cursor:setHeight(h) + self.h = h + self.w = self.h / 3 + self.line_w = math.floor(self.h / self.line_width_factor) +end + +function Cursor:_draw(x, y) + local up_down_width = math.floor(self.line_w / 2) + local body_h = self.h - (up_down_width * 2) + -- paint upper horizontal line + fb.bb:invertRect(x, y, self.w, up_down_width) + -- paint middle vertical line + fb.bb:invertRect(x + (self.w / 2) - up_down_width, y + up_down_width, + self.line_w, body_h) + -- paint lower horizontal line + fb.bb:invertRect(x, y + body_h + up_down_width, self.w, up_down_width) +end + +function Cursor:draw() + if self.is_cleared then + self.is_cleared = false + self:_draw(self.x_pos, self.y_pos) + end +end + +function Cursor:clear() + if not self.is_cleared then + self.is_cleared = true + self:_draw(self.x_pos, self.y_pos) + end +end + +function Cursor:move(x_off, y_off) + self.x_pos = self.x_pos + x_off + self.y_pos = self.y_pos + y_off +end + +function Cursor:moveHorizontal(x_off) + self.x_pos = self.x_pos + x_off +end + +function Cursor:moveVertical(x_off) + self.y_pos = self.y_pos + y_off +end + +function Cursor:moveAndDraw(x_off, y_off) + self:clear() + self:move(x_off, y_off) + self:draw() +end + +function Cursor:moveTo(x_pos, y_pos) + self.x_pos = x_pos + self.y_pos = y_pos +end + +function Cursor:moveToAndDraw(x_pos, y_pos) + self:clear() + self.x_pos = x_pos + self.y_pos = y_pos + self:draw() +end + +function Cursor:moveHorizontalAndDraw(x_off) + self:clear() + self:move(x_off, 0) + self:draw() +end + +function Cursor:moveVerticalAndDraw(y_off) + self:clear() + self:move(0, y_off) + self:draw() +end + diff --git a/inputbox.lua b/inputbox.lua index 6e3fc6667..abbaeb687 100644 --- a/inputbox.lua +++ b/inputbox.lua @@ -4,6 +4,8 @@ require "graphics" InputBox = { -- Class vars: + h = 100, + input_slot_w = nil, input_start_x = 145, input_start_y = nil, input_cur_x = nil, -- points to the start of next input pos @@ -15,44 +17,77 @@ InputBox = { shiftmode = false, altmode = false, + cursor = nil, + -- font for displaying input content + -- we have to use mono here for better distance controlling face = freetype.newBuiltinFace("mono", 25), fhash = "m25", fheight = 25, - fwidth = 16, + fwidth = 15, } -function InputBox:setDefaultInput(text) - self.input_string = "" - self:addString(text) - --self.input_cur_x = self.input_start_x + (string.len(text) * self.fwidth) - --self.input_string = text -end - -function InputBox:addString(str) - for i = 1, #str do - self:addChar(str:sub(i,i)) - end +function InputBox:refreshText() + -- clear previous painted text + fb.bb:paintRect(140, self.input_start_y-19, + self.input_slot_w, self.fheight, self.input_bg) + -- paint new text + renderUtf8Text(fb.bb, self.input_start_x, self.input_start_y, + self.face, self.fhash, + self.input_string, 0) end function InputBox:addChar(char) - renderUtf8Text(fb.bb, self.input_cur_x, self.input_start_y, self.face, self.fhash, - char, true) - fb:refresh(1, self.input_cur_x, self.input_start_y-19, self.fwidth, self.fheight) + self.cursor:clear() + + -- draw new text + local cur_index = (self.cursor.x_pos + 3 - self.input_start_x) + / self.fwidth + self.input_string = self.input_string:sub(1,cur_index)..char.. + self.input_string:sub(cur_index+1) + self:refreshText() self.input_cur_x = self.input_cur_x + self.fwidth - self.input_string = self.input_string .. char + -- draw new cursor + self.cursor:moveHorizontal(self.fwidth) + self.cursor:draw() + + fb:refresh(1, self.input_start_x-5, self.input_start_y-25, + self.input_slot_w, self.h-25) end function InputBox:delChar() if self.input_start_x == self.input_cur_x then return end + + local cur_index = (self.cursor.x_pos + 3 - self.input_start_x) + / self.fwidth + if cur_index == 0 then return end + + self.cursor:clear() + + -- draw new text + self.input_string = self.input_string:sub(0,cur_index-1).. + self.input_string:sub(cur_index+1, -1) + self:refreshText() self.input_cur_x = self.input_cur_x - self.fwidth - --fill last character with blank rectangle - fb.bb:paintRect(self.input_cur_x, self.input_start_y-19, - self.fwidth, self.fheight, self.input_bg) - fb:refresh(1, self.input_cur_x, self.input_start_y-19, self.fwidth, self.fheight) - self.input_string = self.input_string:sub(0,-2) + -- draw new cursor + self.cursor:moveHorizontal(-self.fwidth) + self.cursor:draw() + + fb:refresh(1, self.input_start_x-5, self.input_start_y-25, + self.input_slot_w, self.h-25) +end + +function InputBox:clearText() + self.cursor:clear() + self.input_string = "" + self:refreshText() + self.cursor.x_pos = self.input_start_x - 3 + self.cursor:draw() + + fb:refresh(1, self.input_start_x-5, self.input_start_y-25, + self.input_slot_w, self.h-25) end function InputBox:drawBox(ypos, w, h, title) @@ -66,35 +101,40 @@ function InputBox:drawBox(ypos, w, h, title) end ---[[ - || d_text default to nil (used to set default text in input slot) ---]] +---------------------------------------------------------------------- +-- InputBox:input() +-- +-- @title: input prompt for the box +-- @d_text: default to nil (used to set default text in input slot) +---------------------------------------------------------------------- function InputBox:input(ypos, height, title, d_text) - local pagedirty = true -- do some initilization + self.h = height self.input_start_y = ypos + 35 self.input_cur_x = self.input_start_x + self.input_slot_w = fb.bb:getWidth() - 170 - if d_text then -- if specified default text, draw it - w = fb.bb:getWidth() - 40 - h = height - 45 - self:drawBox(ypos, w, h, title) - self:setDefaultInput(d_text) - fb:refresh(1, 20, ypos, w, h) - pagedirty = false - else -- otherwise, leave the draw task to the main loop - self.input_string = "" + self.cursor = Cursor:new { + x_pos = self.input_start_x - 3, + y_pos = ypos + 13, + h = 30, + } + + + -- draw box and content + w = fb.bb:getWidth() - 40 + h = height - 45 + self:drawBox(ypos, w, h, title) + if d_text then + self.input_string = d_text + self.input_cur_x = self.input_cur_x + (self.fwidth * d_text:len()) + self.cursor.x_pos = self.cursor.x_pos + (self.fwidth * d_text:len()) + self:refreshText() end + self.cursor:draw() + fb:refresh(1, 20, ypos, w, h) while true do - if pagedirty then - w = fb.bb:getWidth() - 40 - h = height - 45 - self:drawBox(ypos, w, h, title) - fb:refresh(1, 20, ypos, w, h) - pagedirty = false - end - local ev = input.waitForEvent() ev.code = adjustKeyEvents(ev) if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then @@ -177,14 +217,30 @@ function InputBox:input(ypos, height, title, d_text) self:addChar(" ") elseif ev.code == KEY_PGFWD then elseif ev.code == KEY_PGBCK then + elseif ev.code == KEY_FW_LEFT then + if (self.cursor.x_pos + 3) > self.input_start_x then + self.cursor:moveHorizontalAndDraw(-self.fwidth) + fb:refresh(1, self.input_start_x-5, ypos, + self.input_slot_w, h) + end + elseif ev.code == KEY_FW_RIGHT then + if (self.cursor.x_pos + 3) < self.input_cur_x then + self.cursor:moveHorizontalAndDraw(self.fwidth) + fb:refresh(1,self.input_start_x-5, ypos, + self.input_slot_w, h) + end elseif ev.code == KEY_ENTER or ev.code == KEY_FW_PRESS then if self.input_string == "" then self.input_string = nil end break elseif ev.code == KEY_DEL then - self:delChar() - elseif ev.code == KEY_BACK then + if Keys.shiftmode then + self:clearText() + else + self:delChar() + end + elseif ev.code == KEY_BACK or ev.code == KEY_HOME then self.input_string = nil break end @@ -195,5 +251,7 @@ function InputBox:input(ypos, height, title, d_text) end -- if end -- while - return self.input_string + local return_str = self.input_string + self.input_string = "" + return return_str end diff --git a/reader.lua b/reader.lua index 1d4e7bb6e..8e6b33af6 100755 --- a/reader.lua +++ b/reader.lua @@ -150,6 +150,7 @@ if ARGV[optind] and lfs.attributes(ARGV[optind], "mode") == "directory" then else if file ~= nil then running = openFile(file) + print(file) else running = false end diff --git a/test/2col.pdf b/test/2col.pdf new file mode 100644 index 000000000..6b91d29ed Binary files /dev/null and b/test/2col.pdf differ diff --git a/unireader.lua b/unireader.lua index 56f96fbf2..e6049e1ea 100644 --- a/unireader.lua +++ b/unireader.lua @@ -34,9 +34,12 @@ UniReader = { -- gamma setting: globalgamma = 1.0, -- GAMMA_NO_GAMMA - -- size of current page for current zoom level in pixels + -- cached tile size fullwidth = 0, fullheight = 0, + -- size of current page for current zoom level in pixels + cur_full_width = 0, + cur_full_height = 0, offset_x = 0, offset_y = 0, min_offset_x = 0, @@ -73,6 +76,7 @@ UniReader = { pagehash = nil, jump_stack = {}, + highlight = {}, toc = nil, bbox = {}, -- override getUsedBBox @@ -85,14 +89,17 @@ function UniReader:new(o) return o end ---[[ - For a new specific reader, - you must always overwrite following two methods: - - * self:open() - - overwrite other methods if needed. ---]] +---------------------------------------------------- +-- !!!!!!!!!!!!!!!!!!!!!!!!! +-- +-- For a new specific reader, +-- you must always overwrite following two methods: +-- +-- * self:open() +-- * self:init() +-- +-- overwrite other methods if needed. +---------------------------------------------------- function UniReader:init() end @@ -102,6 +109,22 @@ function UniReader:open(filename, password) return false end +---------------------------------------------------- +-- You need to overwrite following two methods if your +-- reader supports highlight feature. +---------------------------------------------------- + +function UniReader:startHighLightMode() + return +end + +function UniReader:highLightText() + return +end + +function UniReader:toggleTextHighLight(word_list) + return +end --[ following are default methods ]-- @@ -118,6 +141,9 @@ function UniReader:loadSettings(filename) local jumpstack = self.settings:readSetting("jumpstack") self.jump_stack = jumpstack or {} + local highlight = self.settings:readSetting("highlight") + self.highlight = highlight or {} + local bbox = self.settings:readSetting("bbox") print("# bbox loaded "..dump(bbox)) self.bbox = bbox @@ -186,7 +212,7 @@ function UniReader:drawOrCache(no, preCache) -- TODO: error handling return nil end - local dc = self:setZoom(page) + local dc = self:setzoom(page, preCache) -- offset_x_in_page & offset_y_in_page is the offset within zoomed page -- they are always positive. @@ -291,7 +317,7 @@ function UniReader:clearCache() end -- set viewer state according to zoom state -function UniReader:setZoom(page) +function UniReader:setzoom(page, preCache) local dc = DrawContext.new() local pwidth, pheight = page:getSize(self.nulldc) print("# page::getSize "..pwidth.."*"..pheight); @@ -421,6 +447,10 @@ function UniReader:setZoom(page) dc:setRotate(self.globalrotate); self.fullwidth, self.fullheight = page:getSize(dc) + if not preCache then -- save current page fullsize + self.cur_full_width = self.fullwidth + self.cur_full_height = self.fullheight + end self.min_offset_x = fb.bb:getWidth() - self.fullwidth self.min_offset_y = fb.bb:getHeight() - self.fullheight if(self.min_offset_x > 0) then @@ -473,6 +503,12 @@ function UniReader:show(no) "), src_off:("..offset_x..", "..offset_y.."), ".. "width:"..width..", height:"..height) fb.bb:blitFrom(bb, dest_x, dest_y, offset_x, offset_y, width, height) + + -- render highlights to page + if self.highlight[no] then + self:toggleTextHighLight(self.highlight[no]) + end + if self.rcount == self.rcountmax then print("full refresh") self.rcount = 1 @@ -692,10 +728,10 @@ function UniReader:showTOC() local menu_items = {} local filtered_toc = {} -- build menu items - for _k,_v in ipairs(self.toc) do + for k,v in ipairs(self.toc) do table.insert(menu_items, - (" "):rep(_v.depth-1)..self:cleanUpTOCTitle(_v.title)) - table.insert(filtered_toc,_v.page) + (" "):rep(v.depth-1)..self:cleanUpTOCTitle(v.title)) + table.insert(filtered_toc,v.page) end toc_menu = SelectMenu:new{ menu_title = "Table of Contents", @@ -712,9 +748,9 @@ end function UniReader:showJumpStack() local menu_items = {} - for _k,_v in ipairs(self.jump_stack) do + for k,v in ipairs(self.jump_stack) do table.insert(menu_items, - _v.datetime.." -> Page ".._v.page.." ".._v.notes) + v.datetime.." -> Page "..v.page.." "..v.notes) end jump_menu = SelectMenu:new{ menu_title = "Jump Keeper (current page: "..self.pageno..")", @@ -730,6 +766,29 @@ function UniReader:showJumpStack() end end +function UniReader:showHighLight() + local menu_items = {} + local highlight_dict = {} + -- build menu items + for k,v in pairs(self.highlight) do + if type(k) == "number" then + for k1,v1 in ipairs(v) do + table.insert(menu_items, v1.text) + table.insert(highlight_dict, {page=k, start=v1[1]}) + end + end + end + toc_menu = SelectMenu:new{ + menu_title = "HighLights", + item_array = menu_items, + no_item_msg = "No HighLight found.", + } + item_no = toc_menu:choose(0, fb.bb:getHeight()) + if item_no then + self:goto(highlight_dict[item_no].page) + end +end + function UniReader:showMenu() local ypos = height - 50 local load_percent = (self.pageno / self.doc:getPages()) @@ -810,6 +869,7 @@ function UniReader:inputLoop() self.settings:savesetting("bbox", self.bbox) self.settings:savesetting("globalzoom", self.globalzoom) self.settings:savesetting("globalzoommode", self.globalzoommode) + self.settings:savesetting("highlight", self.highlight) self.settings:close() end @@ -976,7 +1036,19 @@ function UniReader:addAllCommands() function(unireader) unireader:screenRotate("anticlockwise") end) - self.commands:add(KEY_HOME,MOD_SHIFT_OR_ALT,"Home", + self.commands:add(KEY_N, nil, "N", + "start highlight mode", + function(unireader) + unireader:startHighLightMode() + unireader:goto(unireader.pageno) + end) + self.commands:add(KEY_N, MOD_SHIFT, "N", + "display all highlights", + function(unireader) + unireader:showHighLight() + unireader:goto(unireader.pageno) + end) + self.commands:add(KEY_HOME,nil,"Home", "exit application", function(unireader) keep_running = false