diff --git a/.gitignore b/.gitignore index 8a050081b..81a4bd850 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,17 @@ lua lua-* .reader.kpdfview.lua mupdf-thirdparty.zip +djvulibre* kpdfview *.o +kindlepdfviewer-*.zip /.cproject /.project /.reader.kpdfview + +kpvcrlib/CMakeCache.txt +kpvcrlib/CMakeFiles/ +kpvcrlib/cmake_install.cmake +kpvcrlib/Makefile diff --git a/.gitmodules b/.gitmodules index 77f6eecd4..ead63e79f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "djvulibre"] path = djvulibre url = git://djvu.git.sourceforge.net/gitroot/djvu/djvulibre.git +[submodule "kpvcrlib/crengine"] + path = kpvcrlib/crengine + url = git://crengine.git.sourceforge.net/gitroot/crengine/crengine diff --git a/Makefile b/Makefile index b09fd5120..41ee1f11d 100644 --- a/Makefile +++ b/Makefile @@ -5,15 +5,21 @@ MUPDFDIR=mupdf MUPDFTARGET=build/debug MUPDFLIBDIR=$(MUPDFDIR)/$(MUPDFTARGET) DJVUDIR=djvulibre +KPVCRLIGDIR=kpvcrlib +CRENGINEDIR=$(KPVCRLIGDIR)/crengine FREETYPEDIR=$(MUPDFDIR)/thirdparty/freetype-2.4.8 LFSDIR=luafilesystem +# must point to directory with *.ttf fonts for crengine +TTF_FONTS_DIR=$(MUPDFDIR)/fonts + # set this to your ARM cross compiler: -CC:=arm-unknown-linux-gnueabi-gcc -CXX:=arm-unknown-linux-gnueabi-g++ -HOST:=arm-unknown-linux-gnueabi +HOST:=arm-none-linux-gnueabi +CC:=$(HOST)-gcc +CXX:=$(HOST)-g++ +STRIP:=$(HOST)-strip ifdef SBOX_UNAME_MACHINE CC:=gcc CXX:=g++ @@ -21,11 +27,18 @@ endif HOSTCC:=gcc HOSTCXX:=g++ -CFLAGS:=-O3 +CFLAGS:=-O3 $(SYSROOT) +CXXFLAGS:=-O3 $(SYSROOT) +LDFLAGS:= $(SYSROOT) ARM_CFLAGS:=-march=armv6 # use this for debugging: #CFLAGS:=-O0 -g +DYNAMICLIBSTDCPP:=-lstdc++ +ifdef STATICLIBSTDCPP + DYNAMICLIBSTDCPP:= +endif + # you can configure an emulation for the (eink) framebuffer here. # the application won't use the framebuffer (and the special e-ink ioctls) # in that case. @@ -55,16 +68,27 @@ KPDFREADER_CFLAGS=$(CFLAGS) -I$(LUADIR)/src -I$(MUPDFDIR)/ MUPDFLIBS := $(MUPDFLIBDIR)/libfitz.a DJVULIBS := $(DJVUDIR)/build/libdjvu/.libs/libdjvulibre.a +CRENGINELIBS := $(CRENGINEDIR)/crengine/libcrengine.a \ + $(CRENGINEDIR)/thirdparty/chmlib/libchmlib.a \ + $(CRENGINEDIR)/thirdparty/libpng/libpng.a \ +# we don't support dictionary lookup corrently + #$(CRENGINEDIR)/thirdparty/antiword/libantiword.a THIRDPARTYLIBS := $(MUPDFLIBDIR)/libfreetype.a \ - $(MUPDFLIBDIR)/libjpeg.a \ - $(MUPDFLIBDIR)/libopenjpeg.a \ - $(MUPDFLIBDIR)/libjbig2dec.a \ - $(MUPDFLIBDIR)/libz.a + $(MUPDFLIBDIR)/libopenjpeg.a \ + $(MUPDFLIBDIR)/libjbig2dec.a \ + $(MUPDFLIBDIR)/libjpeg.a \ + $(MUPDFLIBDIR)/libz.a + +#@TODO patch crengine to use the latest libjpeg 04.04 2012 (houqp) + #$(MUPDFLIBDIR)/libjpeg.a \ + #$(CRENGINEDIR)/thirdparty/libjpeg/libjpeg.a \ LUALIB := $(LUADIR)/src/liblua.a -kpdfview: kpdfview.o einkfb.o pdf.o blitbuffer.o drawcontext.o input.o util.o ft.o lfs.o $(MUPDFLIBS) $(THIRDPARTYLIBS) $(LUALIB) $(DJVULIBS) djvu.o - $(CC) -lm -ldl -lpthread $(EMU_LDFLAGS) -lstdc++ \ +all:kpdfview + +kpdfview: kpdfview.o einkfb.o pdf.o blitbuffer.o drawcontext.o input.o util.o ft.o lfs.o mupdfimg.o $(MUPDFLIBS) $(THIRDPARTYLIBS) $(LUALIB) djvu.o $(DJVULIBS) cre.o $(CRENGINELIBS) + $(CC) -lm -ldl -lpthread $(EMU_LDFLAGS) $(DYNAMICLIBSTDCPP) \ kpdfview.o \ einkfb.o \ pdf.o \ @@ -74,44 +98,67 @@ kpdfview: kpdfview.o einkfb.o pdf.o blitbuffer.o drawcontext.o input.o util.o ft util.o \ ft.o \ lfs.o \ + mupdfimg.o \ $(MUPDFLIBS) \ $(THIRDPARTYLIBS) \ $(LUALIB) \ djvu.o \ $(DJVULIBS) \ + cre.o \ + $(CRENGINELIBS) \ + $(STATICLIBSTDCPP) \ -o kpdfview -einkfb.o input.o: %.o: %.c - $(CC) -c $(KPDFREADER_CFLAGS) $(EMU_CFLAGS) $< -o $@ +slider_watcher: slider_watcher.c + $(CC) $(CFLAGS) $< -o $@ ft.o: %.o: %.c $(CC) -c $(KPDFREADER_CFLAGS) -I$(FREETYPEDIR)/include -I$(MUPDFDIR)/fitz $< -o $@ -kpdfview.o pdf.o blitbuffer.o util.o drawcontext.o: %.o: %.c - $(CC) -c $(KPDFREADER_CFLAGS) -I$(LFSDIR)/src $< -o $@ +kpdfview.o pdf.o blitbuffer.o util.o drawcontext.o einkfb.o input.o mupdfimg.o: %.o: %.c + $(CC) -c $(KPDFREADER_CFLAGS) $(EMU_CFLAGS) -I$(LFSDIR)/src $< -o $@ djvu.o: %.o: %.c $(CC) -c $(KPDFREADER_CFLAGS) -I$(DJVUDIR)/ $< -o $@ +cre.o: %.o: %.cpp + $(CC) -c -I$(CRENGINEDIR)/crengine/include/ -Ilua/src $< -o $@ -lstdc++ + lfs.o: $(LFSDIR)/src/lfs.c $(CC) -c $(CFLAGS) -I$(LUADIR)/src -I$(LFSDIR)/src $(LFSDIR)/src/lfs.c -o $@ fetchthirdparty: - -rm -Rf lua lua-5.1.4* + -rm -Rf lua lua-5.1.4 -rm -Rf mupdf/thirdparty + test -d mupdf && (cd mupdf; git checkout .) git submodule init git submodule update + ln -sf kpvcrlib/crengine/cr3gui/data data + test -d fonts || ln -sf $(TTF_FONTS_DIR) fonts + # CREngine patch: disable fontconfig + grep USE_FONTCONFIG $(CRENGINEDIR)/crengine/include/crsetup.h && grep -v USE_FONTCONFIG $(CRENGINEDIR)/crengine/include/crsetup.h > /tmp/new && mv /tmp/new $(CRENGINEDIR)/crengine/include/crsetup.h test -f mupdf-thirdparty.zip || wget http://www.mupdf.com/download/mupdf-thirdparty.zip unzip mupdf-thirdparty.zip -d mupdf + # dirty patch in MuPDF's thirdparty liby for CREngine + cd mupdf/thirdparty/jpeg-*/ && \ + patch -N -p0 < ../../../kpvcrlib/jpeg_compress_struct_size.patch &&\ + patch -N -p0 < ../../../kpvcrlib/jpeg_decompress_struct_size.patch + # MuPDF patch: use external fonts + cd mupdf && patch -N -p1 < ../mupdf.patch test -f lua-5.1.4.tar.gz || wget http://www.lua.org/ftp/lua-5.1.4.tar.gz tar xvzf lua-5.1.4.tar.gz && ln -s lua-5.1.4 lua clean: - -rm -f *.o kpdfview + -rm -f *.o kpdfview slider_watcher cleanthirdparty: make -C $(LUADIR) clean make -C $(MUPDFDIR) clean + #make -C $(CRENGINEDIR)/thirdparty/antiword clean + make -C $(CRENGINEDIR)/thirdparty/chmlib clean + make -C $(CRENGINEDIR)/thirdparty/libpng clean + make -C $(CRENGINEDIR)/crengine clean + make -C $(KPVCRLIGDIR) clean -rm -rf $(DJVUDIR)/build -rm -f $(MUPDFDIR)/fontdump.host -rm -f $(MUPDFDIR)/cmapdump.host @@ -128,21 +175,26 @@ $(MUPDFDIR)/cmapdump.host: $(MUPDFLIBS) $(THIRDPARTYLIBS): $(MUPDFDIR)/cmapdump.host $(MUPDFDIR)/fontdump.host # build only thirdparty libs, libfitz and pdf utils, which will care for libmupdf.a being built - CFLAGS="$(CFLAGS)" make -C mupdf CC="$(CC)" CMAPDUMP=cmapdump.host FONTDUMP=fontdump.host MUPDF= XPS_APPS= + CFLAGS="$(CFLAGS) -DNOBUILTINFONT" make -C mupdf CC="$(CC)" CMAPDUMP=cmapdump.host FONTDUMP=fontdump.host MUPDF= XPS_APPS= verbose=1 $(DJVULIBS): -mkdir $(DJVUDIR)/build ifdef EMULATE_READER cd $(DJVUDIR)/build && ../configure --disable-desktopfiles --disable-shared --enable-static else - cd $(DJVUDIR)/build && ../configure --disable-desktopfiles --disable-shared --enable-static --host=$(HOST) + cd $(DJVUDIR)/build && ../configure --disable-desktopfiles --disable-shared --enable-static --host=$(HOST) --disable-xmltools --disable-desktopfiles endif make -C $(DJVUDIR)/build +$(CRENGINELIBS): + cd $(KPVCRLIGDIR) && rm -rf CMakeCache.txt CMakeFiles && \ + CFLAGS="$(CFLAGS)" CC="$(CC)" CXX="$(CXX)" cmake . && \ + make + $(LUALIB): make -C lua/src CC="$(CC)" CFLAGS="$(CFLAGS)" MYCFLAGS=-DLUA_USE_LINUX MYLIBS="-Wl,-E" liblua.a -thirdparty: $(MUPDFLIBS) $(THIRDPARTYLIBS) $(LUALIB) $(DJVULIBS) +thirdparty: $(MUPDFLIBS) $(THIRDPARTYLIBS) $(LUALIB) $(DJVULIBS) $(CRENGINELIBS) INSTALL_DIR=kindlepdfviewer @@ -152,11 +204,19 @@ install: scp launchpad/* root@192.168.2.2:/mnt/us/launchpad/ VERSION?=$(shell git rev-parse --short HEAD) -customupdate: kpdfview +customupdate: all # ensure that build binary is for ARM file kpdfview | grep ARM || exit 1 + $(STRIP) --strip-unneeded kpdfview + -rm kindlepdfviewer-$(VERSION).zip + rm -Rf $(INSTALL_DIR) mkdir $(INSTALL_DIR) cp -p README.TXT COPYING kpdfview *.lua $(INSTALL_DIR) - zip -r kindlepdfviewer-$(VERSION).zip $(INSTALL_DIR) launchpad/ + mkdir $(INSTALL_DIR)/data + cp -rpL data/*.css $(INSTALL_DIR)/data + cp -rpL fonts $(INSTALL_DIR) + cp -r resources $(INSTALL_DIR) + mkdir $(INSTALL_DIR)/fonts/host + zip -9 -r kindlepdfviewer-$(VERSION).zip $(INSTALL_DIR) launchpad/ rm -Rf $(INSTALL_DIR) @echo "copy kindlepdfviewer-$(VERSION).zip to /mnt/us/customupdates and install with shift+shift+I" diff --git a/blitbuffer.c b/blitbuffer.c index 248db52ed..b4469314f 100644 --- a/blitbuffer.c +++ b/blitbuffer.c @@ -366,6 +366,124 @@ 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 int dimRect(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 dimm 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++) { + int px = *dstptr & 0x0F; + *dstptr &= 0xF0 | px >> 1; + 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) = + ( *(dstptr+cx) >> 1 ) & 0xF0 | + ( *(dstptr+cx) & 0x0F ) >> 1; + } + dstptr += dst->pitch; + } + if(w & 1) { + /* This will dimm 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++) { + int px = *dstptr & 0xF0; + *dstptr &= 0x0F | ( px >> 1 & 0xF0 ); + dstptr += dst->pitch; + } + } + return 0; +} + static const struct luaL_Reg blitbuffer_func[] = { {"new", newBlitBuffer}, {NULL, NULL} @@ -378,6 +496,8 @@ static const struct luaL_Reg blitbuffer_meth[] = { {"addblitFrom", addblitToBuffer}, {"blitFullFrom", blitFullToBuffer}, {"paintRect", paintRect}, + {"invertRect", invertRect}, + {"dimRect", dimRect}, {"free", freeBlitBuffer}, {"__gc", freeBlitBuffer}, {NULL, NULL} diff --git a/commands.lua b/commands.lua index 09611a77c..c47215b8b 100644 --- a/commands.lua +++ b/commands.lua @@ -5,6 +5,7 @@ Keydef = { modifier = nil, descr = nil } + function Keydef:_new(obj) -- obj definition obj = obj or {} @@ -13,6 +14,7 @@ function Keydef:_new(obj) self.__tostring=Keydef.tostring return obj end + function Keydef:new(keycode,modifier,descr) obj = Keydef:_new() obj.keycode = keycode @@ -20,13 +22,16 @@ function Keydef:new(keycode,modifier,descr) obj.descr = descr return obj end + function Keydef:display() return (self.modifier or "")..(self.descr or "") end + function Keydef:tostring() return ((self.modifier and self.modifier.."+") or "").."["..(self.keycode or "").."]"..(self.descr or "") end + Command = { keydef = nil, keygroup = nil, @@ -34,6 +39,7 @@ Command = { help = nil, order = nil } + function Command:_new(obj) -- obj definition obj = obj or {} @@ -42,6 +48,7 @@ function Command:_new(obj) self.__tostring=Command.tostring return obj end + function Command:new(keydef, func, help, keygroup, order) obj = Command:_new() obj.keydef = keydef @@ -52,6 +59,7 @@ function Command:new(keydef, func, help, keygroup, order) --print("creating command: ["..tostring(keydef).."] keygroup:["..(keygroup or "").."] help:"..help) return obj end + function Command:tostring() return tostring(self.keydef)..": "..(self.help or "") end @@ -61,15 +69,54 @@ Commands = { map = {}, size = 0 } + function Commands:add(keycode,modifier,keydescr,help,func) - local keydef = Keydef:new(keycode,modifier,keydescr) - self:_addImpl(keydef,help,func) + if type(keycode) == "table" then + for i=1,#keycode,1 do + local keydef = Keydef:new(keycode[i],modifier,keydescr) + self:_addImpl(keydef,help,func) + end + else + local keydef = Keydef:new(keycode,modifier,keydescr) + self:_addImpl(keydef,help,func) + end end + function Commands:addGroup(keygroup,keys,help,func) for _k,keydef in pairs(keys) do self:_addImpl(keydef,help,func,keygroup) end end + +--@TODO handle MOD_ANY 06.04 2012 (houqp) +function Commands:del(keycode, modifier, keydescr) + local keydef = nil + + if not keydescr then + for k,v in pairs(self.map) do + if v.keydef.keycode == keycode + and v.keydef.modifier == modifier then + keydef = k + break + end + end -- EOF for + else + keydef = Keydef:new(keycode, modifier, keydescr) + end -- EOF if + + self.map[keydef] = nil +end + +function Commands:delGroup(keygroup) + if keygroup then + for k,v in pairs(self.map) do + if v.keygroup == keygroup then + self.map[k] = nil + end + end -- EOF for + end +end + function Commands:_addImpl(keydef,help,func,keygroup) if keydef.modifier==MOD_ANY then self:addGroup(keygroup or keydef.descr,{Keydef:new(keydef.keycode,nil), Keydef:new(keydef.keycode,MOD_SHIFT), Keydef:new(keydef.keycode,MOD_ALT)},help,func) @@ -88,25 +135,51 @@ function Commands:_addImpl(keydef,help,func,keygroup) end end end + function Commands:get(keycode,modifier) return self.map[Keydef:new(keycode, modifier)] end + function Commands:getByKeydef(keydef) return self.map[keydef] end + function Commands:new(obj) - -- payload - local mt = {} - setmetatable(self.map,mt) - mt.__index=function (table, key) - return rawget(table,(key.modifier or "").."@#@"..(key.keycode or "")) - end - mt.__newindex=function (table, key, value) - return rawset(table,(key.modifier or "").."@#@"..(key.keycode or ""),value) - end -- obj definition obj = obj or {} + obj.map = {} + obj.size = 0 setmetatable(obj, self) self.__index = self + + -- payload + local mt = {} + mt.__index = function(table, key) + return rawget(table,(key.modifier or "").."@#@"..(key.keycode or "")) + end + mt.__newindex = function(table, key, value) + return rawset(table,(key.modifier or "").."@#@"..(key.keycode or ""),value) + end + setmetatable(obj.map, mt) + + obj:add(KEY_INTO_SCREEN_SAVER, nil, "Slider", + "toggle screen saver", + function() + Screen:saveCurrentBB() + Screen.kpv_rotation_mode = Screen.cur_rotation_mode + fb:setOrientation(Screen.native_rotation_mode) + --os.execute("killall -cont cvm") + end + ) + obj:add(KEY_OUTOF_SCREEN_SAVER, nil, "Slider", + "toggle screen saver", + function() + util.sleep(3) + --os.execute("killall -stop cvm") + fb:setOrientation(Screen.kpv_rotation_mode) + Screen:restoreFromSavedBB() + fb:refresh(0) + end + ) return obj end diff --git a/cre.cpp b/cre.cpp new file mode 100644 index 000000000..feea80114 --- /dev/null +++ b/cre.cpp @@ -0,0 +1,486 @@ +/* + KindlePDFViewer: CREngine abstraction for Lua + Copyright (C) 2012 Hans-Werner Hilse + Qingping Hou + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +extern "C" { +#include "blitbuffer.h" +#include "drawcontext.h" +#include "cre.h" +} + +#include "crengine.h" + + +typedef struct CreDocument { + LVDocView *text_view; + ldomDocument *dom_doc; +} CreDocument; + + +static int openDocument(lua_State *L) { + const char *file_name = luaL_checkstring(L, 1); + const char *style_sheet = luaL_checkstring(L, 2); + + int width = luaL_checkint(L, 3); + int height = luaL_checkint(L, 4); + lString8 css; + + CreDocument *doc = (CreDocument*) lua_newuserdata(L, sizeof(CreDocument)); + luaL_getmetatable(L, "credocument"); + lua_setmetatable(L, -2); + + doc->text_view = new LVDocView(); + //doc->text_view->setBackgroundColor(0xFFFFFF); + //doc->text_view->setTextColor(0x000000); + if (LVLoadStylesheetFile(lString16(style_sheet), css)){ + if (!css.empty()){ + doc->text_view->setStyleSheet(css); + } + } + doc->text_view->setViewMode(DVM_SCROLL, -1); + doc->text_view->Resize(width, height); + doc->text_view->LoadDocument(file_name); + doc->dom_doc = doc->text_view->getDocument(); + doc->text_view->Render(); + + return 1; +} + +static int getGammaIndex(lua_State *L) { + lua_pushinteger(L, fontMan->GetGammaIndex()); + + return 1; +} + +static int setGammaIndex(lua_State *L) { + int index = luaL_checkint(L, 1); + + fontMan->SetGammaIndex(index); + + return 0; +} + +static int closeDocument(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + delete doc->text_view; + + return 0; +} + +static int getNumberOfPages(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + + lua_pushinteger(L, doc->text_view->getPageCount()); + + return 1; +} + +static int getCurrentPage(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + + lua_pushinteger(L, doc->text_view->getCurPage()); + + return 1; +} + +static int getPageFromXPointer(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + const char *xpointer_str = luaL_checkstring(L, 2); + + int page = 0; + ldomXPointer xp = doc->dom_doc->createXPointer(lString16(xpointer_str)); + + page = doc->text_view->getBookmarkPage(xp); + lua_pushinteger(L, page); + + return 1; +} + +static int getCurrentPos(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + + lua_pushinteger(L, doc->text_view->GetPos()); + + return 1; +} + +//static int getPosFromXPointer(lua_State *L) { + //CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + //const char *xpointer_str = luaL_checkstring(L, 2); + + //lvRect rc; + //int pos; + + //ldomXPointer *xp = NULL; + //xp = doc->dom_doc->createXPointer(lString16(xpointer_str)); + //getCursorDocRect(*xp, rc); + //pos = + + //return 1; +//} + +static int getCurrentPercent(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + + lua_pushinteger(L, doc->text_view->getPosPercent()); + + return 1; +} + +static int getXPointer(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + + ldomXPointer xp = doc->text_view->getBookmark(); + lua_pushstring(L, UnicodeToLocal(xp.toString()).c_str()); + + return 1; +} + +static int getFullHeight(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + + lua_pushinteger(L, doc->text_view->GetFullHeight()); + + return 1; +} + +/* + * helper function for getTableOfContent() + */ +static int walkTableOfContent(lua_State *L, LVTocItem *toc, int *count) { + LVTocItem *toc_tmp = NULL; + int i = 0, + nr_child = toc->getChildCount(); + + for(i = 0; i < nr_child; i++) { + toc_tmp = toc->getChild(i); + lua_pushnumber(L, (*count)++); + + /* set subtable, Toc entry */ + lua_newtable(L); + lua_pushstring(L, "page"); + lua_pushnumber(L, toc_tmp->getPercent()); + lua_settable(L, -3); + + lua_pushstring(L, "xpointer"); + lua_pushstring(L, UnicodeToLocal( + toc_tmp->getXPointer().toString()).c_str() + ); + lua_settable(L, -3); + + lua_pushstring(L, "depth"); + lua_pushnumber(L, toc_tmp->getLevel()); + lua_settable(L, -3); + + lua_pushstring(L, "title"); + lua_pushstring(L, UnicodeToLocal(toc_tmp->getName()).c_str()); + lua_settable(L, -3); + + + /* set Toc entry to Toc table */ + lua_settable(L, -3); + + if (toc_tmp->getChildCount() > 0) { + walkTableOfContent(L, toc_tmp, count); + } + } + return 0; +} + +/* + * Return a table like this: + * { + * { + * page=12, + * xpointer = "/body/DocFragment[11].0", + * depth=1, + * title="chapter1" + * }, + * { + * page=54, + * xpointer = "/body/DocFragment[13].0", + * depth=1, + * title="chapter2" + * }, + * } + * + * Warnning: not like pdf or djvu support, page here refers to the + * percent of height within the document, not the real page number. + * We use page here just to keep consistent with other readers. + * + */ +static int getTableOfContent(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + + LVTocItem * toc = doc->text_view->getToc(); + int count = 0; + + lua_newtable(L); + walkTableOfContent(L, toc, &count); + + return 1; +} + +/* + * Return a table like this: + * { + * "FreeMono", + * "FreeSans", + * "FreeSerif", + * } + * + */ +static int getFontFaces(lua_State *L) { + int i = 0; + lString16Collection face_list; + + fontMan->getFaceList(face_list); + + lua_newtable(L); + for (i = 0; i < face_list.length(); i++) + { + lua_pushnumber(L, i+1); + lua_pushstring(L, UnicodeToLocal(face_list[i]).c_str()); + lua_settable(L, -3); + } + + return 1; +} + +static int setFontFace(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + const char *face = luaL_checkstring(L, 2); + + doc->text_view->setDefaultFontFace(lString8(face)); + + return 0; +} + +static int gotoPage(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + int pageno = luaL_checkint(L, 2); + + doc->text_view->goToPage(pageno); + + return 0; +} + +static int gotoPercent(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + int percent = luaL_checkint(L, 2); + + doc->text_view->SetPos(percent * doc->text_view->GetFullHeight() / 10000); + + return 0; +} + +static int gotoPos(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + int pos = luaL_checkint(L, 2); + + doc->text_view->SetPos(pos); + + return 0; +} + +static int gotoXPointer(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + const char *xpointer_str = luaL_checkstring(L, 2); + + ldomXPointer xp = doc->dom_doc->createXPointer(lString16(xpointer_str)); + + doc->text_view->goToBookmark(xp); + /* CREngine does not call checkPos() immediately after goToBookmark, + * so I have to manually update the pos in order to get a correct + * return from GetPos() call. */ + doc->text_view->SetPos(xp.toPoint().y); + + return 0; +} + +/* zoom font by given delta and return zoomed font size */ +static int zoomFont(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + int delta = luaL_checkint(L, 2); + + doc->text_view->ZoomFont(delta); + + lua_pushnumber(L, doc->text_view->getFontSize()); + return 1; +} + +static int setFontSize(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + int size = luaL_checkint(L, 2); + + doc->text_view->setFontSize(size); + return 0; +} + +static int setDefaultInterlineSpace(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + int space = luaL_checkint(L, 2); + + doc->text_view->setDefaultInterlineSpace(space); + return 0; +} + +static int setStyleSheet(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + const char* style_sheet_data = luaL_checkstring(L, 2); + + doc->text_view->setStyleSheet(lString8(style_sheet_data)); + return 0; +} + +static int toggleFontBolder(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + + doc->text_view->doCommand(DCMD_TOGGLE_BOLD); + + return 0; +} + +static int cursorRight(lua_State *L) { + //CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + + //LVDocView *tv = doc->text_view; + + //ldomXPointer p = tv->getCurrentPageMiddleParagraph(); + //lString16 s = p.toString(); + //printf("~~~~~~~~~~%s\n", UnicodeToLocal(s).c_str()); + + //tv->selectRange(*(tv->selectFirstPageLink())); + //ldomXRange *r = tv->selectNextPageLink(true); + //lString16 s = r->getRangeText(); + //printf("------%s\n", UnicodeToLocal(s).c_str()); + + //tv->selectRange(*r); + //tv->updateSelections(); + + //LVPageWordSelector sel(doc->text_view); + //doc->text_view->doCommand(DCMD_SELECT_FIRST_SENTENCE); + //sel.moveBy(DIR_RIGHT, 2); + //sel.updateSelection(); + //printf("---------------- %s\n", UnicodeToLocal(sel.getSelectedWord()->getText()).c_str()); + + return 0; +} + +static int drawCurrentPage(lua_State *L) { + CreDocument *doc = (CreDocument*) luaL_checkudata(L, 1, "credocument"); + DrawContext *dc = (DrawContext*) luaL_checkudata(L, 2, "drawcontext"); + BlitBuffer *bb = (BlitBuffer*) luaL_checkudata(L, 3, "blitbuffer"); + + int w = bb->w, + h = bb->h; + /* Set DrawBuf to 4bpp */ + LVGrayDrawBuf drawBuf(w, h, 4); + + doc->text_view->Resize(w, h); + doc->text_view->Render(); + doc->text_view->Draw(drawBuf); + + uint8_t *bbptr = (uint8_t*)bb->data; + uint8_t *pmptr = (uint8_t*)drawBuf.GetScanLine(0); + int i,x; + + for (i = 0; i < h; i++) { + for (x = 0; x < (bb->w / 2); x++) { + /* When DrawBuf is set to 4bpp mode, CREngine still put every + * four bits in one byte, but left the last 4 bits zero*/ + bbptr[x] = ~(pmptr[x*2] | (pmptr[x*2+1] >> 4)); + } + if(bb->w & 1) { + bbptr[x] = 255 - (pmptr[x*2] & 0xF0); + } + bbptr += bb->pitch; + pmptr += w; + } + + return 0; +} + +static int registerFont(lua_State *L) { + const char *fontfile = luaL_checkstring(L, 1); + if ( !fontMan->RegisterFont(lString8(fontfile)) ) { + return luaL_error(L, "cannot register font <%s>", fontfile); + } + return 0; +} + +static const struct luaL_Reg cre_func[] = { + {"openDocument", openDocument}, + {"getFontFaces", getFontFaces}, + {"getGammaIndex", getGammaIndex}, + {"setGammaIndex", setGammaIndex}, + {"registerFont", registerFont}, + {NULL, NULL} +}; + +static const struct luaL_Reg credocument_meth[] = { + /*--- get methods ---*/ + {"getPages", getNumberOfPages}, + {"getCurrentPage", getCurrentPage}, + {"getPageFromXPointer", getPageFromXPointer}, + {"getCurrentPos", getCurrentPos}, + {"getCurrentPercent", getCurrentPercent}, + {"getXPointer", getXPointer}, + {"getFullHeight", getFullHeight}, + {"getToc", getTableOfContent}, + /*--- set methods ---*/ + {"setFontFace", setFontFace}, + {"setFontSize", setFontSize}, + {"setDefaultInterlineSpace", setDefaultInterlineSpace}, + {"setStyleSheet", setStyleSheet}, + /* --- control methods ---*/ + {"gotoPage", gotoPage}, + {"gotoPercent", gotoPercent}, + {"gotoPos", gotoPos}, + {"gotoXPointer", gotoXPointer}, + {"zoomFont", zoomFont}, + {"toggleFontBolder", toggleFontBolder}, + //{"cursorLeft", cursorLeft}, + //{"cursorRight", cursorRight}, + {"drawCurrentPage", drawCurrentPage}, + {"close", closeDocument}, + {"__gc", closeDocument}, + {NULL, NULL} +}; + +int luaopen_cre(lua_State *L) { + luaL_newmetatable(L, "credocument"); + lua_pushstring(L, "__index"); + lua_pushvalue(L, -2); + lua_settable(L, -3); + luaL_register(L, NULL, credocument_meth); + lua_pop(L, 1); + luaL_register(L, "cre", cre_func); + + + /* initialize font manager for CREngine */ + InitFontManager(lString8()); + +#ifdef DEBUG_CRENGINE + CRLog::setStdoutLogger(); + CRLog::setLogLevel(CRLog::LL_DEBUG); +#endif + + return 1; +} diff --git a/cre.h b/cre.h new file mode 100644 index 000000000..f4166095e --- /dev/null +++ b/cre.h @@ -0,0 +1,28 @@ +/* + KindlePDFViewer: CREngine abstraction for Lua + Copyright (C) 2012 Hans-Werner Hilse + Qingping Hou + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef _CRENGING_H +#define _CRENGING_H + +#include +#include +#include + +int luaopen_cre(lua_State *L); +#endif diff --git a/crereader.lua b/crereader.lua new file mode 100644 index 000000000..dab931cec --- /dev/null +++ b/crereader.lua @@ -0,0 +1,375 @@ +require "font" +require "unireader" +require "inputbox" +require "selectmenu" + +CREReader = UniReader:new{ + pos = nil, + percent = 0, + + gamma_index = 15, + font_face = nil, + + line_space_percent = 100, +} + +function CREReader:init() + self:addAllCommands() + self:adjustCreReaderCommands() + -- we need to initialize the CRE font list + local fonts = Font:getFontList() + for _k, _v in ipairs(fonts) do + local ok, err = pcall(cre.registerFont, Font.fontdir..'/'.._v) + if not ok then + print(err) + end + end +end + +-- open a CREngine supported file and its settings store +function CREReader:open(filename) + local ok + local file_type = string.lower(string.match(filename, ".+%.([^.]+)")) + -- these two format use the same css file + if file_type == "html" then + file_type = "htm" + end + local style_sheet = "./data/"..file_type..".css" + ok, self.doc = pcall(cre.openDocument, filename, style_sheet, + G_width, G_height) + if not ok then + return false, self.doc -- will contain error message + end + + self.doc:setDefaultInterlineSpace(self.line_space_percent) + + return true +end + +---------------------------------------------------- +-- setting related methods +---------------------------------------------------- +function CREReader:loadSpecialSettings() + local font_face = self.settings:readSetting("font_face") + self.font_face = font_face or "FreeSerif" + self.doc:setFontFace(self.font_face) + + local gamma_index = self.settings:readSetting("gamma_index") + self.gamma_index = gamma_index or self.gamma_index + cre.setGammaIndex(self.gamma_index) + + local line_space_percent = self.settings:readSetting("line_space_percent") + self.line_space_percent = line_space_percent or self.line_space_percent +end + +function CREReader:getLastPageOrPos() + local last_percent = self.settings:readSetting("last_percent") + if last_percent then + return math.floor((last_percent * self.doc:getFullHeight()) / 10000) + else + return 0 + end +end + +function CREReader:saveSpecialSettings() + self.settings:savesetting("font_face", self.font_face) + self.settings:savesetting("gamma_index", self.gamma_index) + self.settings:savesetting("line_space_percent", self.line_space_percent) +end + +function CREReader:saveLastPageOrPos() + self.settings:savesetting("last_percent", self.percent) +end + +---------------------------------------------------- +-- render related methods +---------------------------------------------------- +-- we don't need setzoom in CREReader +function CREReader:setzoom(page, preCache) + return +end + +function CREReader:redrawCurrentPage() + self:goto(self.pos) +end + +-- there is no zoom mode in CREReader +function CREReader:setGlobalZoomMode() + return +end + +---------------------------------------------------- +-- goto related methods +---------------------------------------------------- +function CREReader:goto(pos, pos_type) + local prev_xpointer = self.doc:getXPointer() + local width, height = G_width, G_height + + if pos_type == "xpointer" then + self.doc:gotoXPointer(pos) + pos = self.doc:getCurrentPos() + else -- pos_type is PERCENT * 100 + pos = math.min(pos, self.doc:getFullHeight() - height) + pos = math.max(pos, 0) + self.doc:gotoPos(pos) + end + + -- add to jump_stack, distinguish jump from normal page turn + -- NOTE: + -- even though we have called gotoPos() or gotoXPointer() previously, + -- self.pos hasn't been updated yet here, so we can still make use of it. + if self.pos and math.abs(self.pos - pos) > height then + self:addJump(prev_xpointer) + end + + self.doc:drawCurrentPage(self.nulldc, fb.bb) + + print("## self.show_overlap "..self.show_overlap) + if self.show_overlap < 0 then + fb.bb:dimRect(0,0, width, -self.show_overlap) + elseif self.show_overlap > 0 then + fb.bb:dimRect(0,height - self.show_overlap, width, self.show_overlap) + end + self.show_overlap = 0 + + if self.rcount == self.rcountmax then + print("full refresh") + self.rcount = 1 + fb:refresh(0) + else + print("partial refresh") + self.rcount = self.rcount + 1 + fb:refresh(1) + end + + self.pos = pos + print("------", self.pos) + self.pageno = self.doc:getCurrentPage() + self.percent = self.doc:getCurrentPercent() +end + +function CREReader:gotoPercent(percent) + self:goto(percent * self.doc:getFullHeight() / 10000) +end + +function CREReader:gotoTocEntry(entry) + self:goto(entry.xpointer, "xpointer") +end + +function CREReader:nextView() + self.show_overlap = -self.pan_overlap_vertical + return self.pos + G_height - self.pan_overlap_vertical +end + +function CREReader:prevView() + self.show_overlap = self.pan_overlap_vertical + return self.pos - G_height + self.pan_overlap_vertical +end + +---------------------------------------------------- +-- jump stack related methods +---------------------------------------------------- +function CREReader:isSamePage(p1, p2) + return self.doc:getPageFromXPointer(p1) == self.doc:getPageFromXPointer(p2) +end + +function CREReader:showJumpStack() + local menu_items = {} + print(dump(self.jump_stack)) + for k,v in ipairs(self.jump_stack) do + table.insert(menu_items, + v.datetime.." -> page ".. + (self.doc:getPageFromXPointer(v.page)).." "..v.notes) + end + jump_menu = SelectMenu:new{ + menu_title = "Jump Keeper (current page: "..self.pageno..")", + item_array = menu_items, + no_item_msg = "No jump history.", + } + item_no = jump_menu:choose(0, fb.bb:getHeight()) + if item_no then + local jump_item = self.jump_stack[item_no] + self:goto(jump_item.page, "xpointer") + else + self:redrawCurrentPage() + end +end + +---------------------------------------------------- +-- TOC related methods +---------------------------------------------------- +function CREReader:getTocTitleOfCurrentPage() + return self:getTocTitleByPage(self.percent) +end + + +---------------------------------------------------- +-- menu related methods +---------------------------------------------------- +-- used in CREReader:showMenu() +function CREReader:_drawReadingInfo() + local ypos = G_height - 50 + local load_percent = self.percent/100 + + fb.bb:paintRect(0, ypos, G_width, 50, 0) + + ypos = ypos + 15 + local face = Font:getFace("rifont", 22) + local cur_section = self:getTocTitleOfCurrentPage() + if cur_section ~= "" then + cur_section = "Section: "..cur_section + end + renderUtf8Text(fb.bb, 10, ypos+6, face, + "Position: "..load_percent.."%".." "..cur_section, true) + + ypos = ypos + 15 + blitbuffer.progressBar(fb.bb, 10, ypos, G_width - 20, 15, + 5, 4, load_percent/100, 8) +end + + + +function CREReader:adjustCreReaderCommands() + -- delete commands + self.commands:delGroup("[joypad]") + self.commands:del(KEY_G, nil, "G") + self.commands:del(KEY_J, MOD_SHIFT, "J") + self.commands:del(KEY_K, MOD_SHIFT, "K") + self.commands:del(KEY_Z, nil, "Z") + self.commands:del(KEY_Z, MOD_SHIFT, "Z") + self.commands:del(KEY_Z, MOD_ALT, "Z") + self.commands:del(KEY_A, nil, "A") + self.commands:del(KEY_A, MOD_SHIFT, "A") + self.commands:del(KEY_A, MOD_ALT, "A") + self.commands:del(KEY_S, nil, "S") + self.commands:del(KEY_S, MOD_SHIFT, "S") + self.commands:del(KEY_S, MOD_ALT, "S") + self.commands:del(KEY_D, nil, "D") + self.commands:del(KEY_D, MOD_SHIFT, "D") + self.commands:del(KEY_D, MOD_ALT, "D") + self.commands:del(KEY_F, MOD_SHIFT, "F") + self.commands:del(KEY_F, MOD_ALT, "F") + self.commands:del(KEY_N, nil, "N") -- highlight + self.commands:del(KEY_N, MOD_SHIFT, "N") -- show highlights + + -- overwrite commands + self.commands:add(KEY_PGFWD, MOD_SHIFT, ">", + "increase font size", + function(cr) + cr.doc:zoomFont(1) + cr:redrawCurrentPage() + end + ) + self.commands:add(KEY_PGBCK, MOD_SHIFT, "<", + "decrease font size", + function(cr) + cr.doc:zoomFont(-1) + cr:redrawCurrentPage() + end + ) + self.commands:add(KEY_PGFWD, MOD_ALT, ">", + "increase line spacing", + function(cr) + self.line_space_percent = self.line_space_percent + 10 + if self.line_space_percent > 200 then + self.line_space_percent = 200 + end + print("line spacing set to", self.line_space_percent) + cr.doc:setDefaultInterlineSpace(self.line_space_percent) + cr:redrawCurrentPage() + end + ) + self.commands:add(KEY_PGBCK, MOD_ALT, "<", + "decrease line spacing", + function(cr) + self.line_space_percent = self.line_space_percent - 10 + if self.line_space_percent < 100 then + self.line_space_percent = 100 + end + print("line spacing set to", self.line_space_percent) + cr.doc:setDefaultInterlineSpace(self.line_space_percent) + cr:redrawCurrentPage() + end + ) + local numeric_keydefs = {} + for i=1,10 do + numeric_keydefs[i]=Keydef:new(KEY_1+i-1, nil, tostring(i%10)) + end + self.commands:addGroup("[1..0]", numeric_keydefs, + "jump to *10% of document", + function(cr, keydef) + print('jump to position: '.. + math.floor(cr.doc:getFullHeight()*(keydef.keycode-KEY_1)/9).. + '/'..cr.doc:getFullHeight()) + cr:goto(math.floor(cr.doc:getFullHeight()*(keydef.keycode-KEY_1)/9)) + end + ) + self.commands:add(KEY_F, nil, "F", + "invoke font menu", + function(cr) + local face_list = cre.getFontFaces() + + local fonts_menu = SelectMenu:new{ + menu_title = "Fonts Menu", + item_array = face_list, + } + + local item_no = fonts_menu:choose(0, G_height) + print(face_list[item_no]) + if item_no then + cr.doc:setFontFace(face_list[item_no]) + self.font_face = face_list[item_no] + end + cr:redrawCurrentPage() + end + ) + self.commands:add(KEY_F, MOD_ALT, "F", + "Toggle font bolder attribute", + function(cr) + cr.doc:toggleFontBolder() + cr:redrawCurrentPage() + end + ) + self.commands:add(KEY_B, MOD_SHIFT, "B", + "add jump", + function(cr) + cr:addJump(self.doc:getXPointer()) + end + ) + self.commands:add(KEY_BACK,nil,"back", + "back to last jump", + function(cr) + if #cr.jump_stack ~= 0 then + cr:goto(cr.jump_stack[1].page, "xpointer") + end + end + ) + self.commands:add(KEY_VPLUS, nil, "vol+", + "increase gamma", + function(cr) + cre.setGammaIndex(self.gamma_index + 1) + self.gamma_index = cre.getGammaIndex() + cr:redrawCurrentPage() + end + ) + self.commands:add(KEY_VMINUS, nil, "vol-", + "decrease gamma", + function(cr) + cre.setGammaIndex(self.gamma_index - 1) + self.gamma_index = cre.getGammaIndex() + cr:redrawCurrentPage() + end + ) + self.commands:add(KEY_FW_UP, nil, "joypad up", + "pan "..self.shift_y.." pixels upwards", + function(cr) + cr:goto(cr.pos - cr.shift_y) + end + ) + self.commands:add(KEY_FW_DOWN, nil, "joypad down", + "pan "..self.shift_y.." pixels downwards", + function(cr) + cr:goto(cr.pos + cr.shift_y) + end + ) +end diff --git a/dialog.lua b/dialog.lua new file mode 100644 index 000000000..7ae25e108 --- /dev/null +++ b/dialog.lua @@ -0,0 +1,43 @@ +require "widget" +require "font" + +InfoMessage = { + face = Font:getFace("infofont", 25) +} + +function InfoMessage:show(text) + local dialog = CenterContainer:new({ + dimen = { w = G_width, h = G_height }, + FrameContainer:new({ + margin = 2, + background = 0, + HorizontalGroup:new({ + align = "center", + ImageWidget:new({ + file = "resources/info-i.png" + }), + Widget:new({ + dimen = { w = 10, h = 0 } + }), + TextWidget:new({ + text = text, + face = Font:getFace("cfont", 30) + }) + }) + }) + }) + dialog:paintTo(fb.bb, 0, 0) + dialog:free() +end + +function showInfoMsgWithDelay(text, msec, refresh_mode) + if not refresh_mode then refresh_mode = 0 end + Screen:saveCurrentBB() + + InfoMessage:show(text) + fb:refresh(refresh_mode) + util.usleep(msec*1000) + + Screen:restoreFromSavedBB() + fb:refresh(refresh_mode) +end diff --git a/djvu.c b/djvu.c index 090d625a3..56a34c32f 100644 --- a/djvu.c +++ b/djvu.c @@ -71,7 +71,7 @@ static int handle(lua_State *L, ddjvu_context_t *ctx, int wait) static int openDocument(lua_State *L) { const char *filename = luaL_checkstring(L, 1); - /*const char *password = luaL_checkstring(L, 2);*/ + int cache_size = luaL_optint(L, 2, 10 << 20); DjvuDocument *doc = (DjvuDocument*) lua_newuserdata(L, sizeof(DjvuDocument)); luaL_getmetatable(L, "djvudocument"); @@ -82,6 +82,9 @@ static int openDocument(lua_State *L) { return luaL_error(L, "cannot create context."); } + printf("## cache_size = %d\n", cache_size); + ddjvu_cache_set_size(doc->context, (unsigned long)cache_size); + doc->doc_ref = ddjvu_document_create_by_filename_utf8(doc->context, filename, TRUE); while (! ddjvu_document_decoding_done(doc->doc_ref)) handle(L, doc->context, True); @@ -206,7 +209,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 +228,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) { @@ -325,6 +450,21 @@ static int drawPage(lua_State *L) { return 0; } +static int getCacheSize(lua_State *L) { + DjvuDocument *doc = (DjvuDocument*) luaL_checkudata(L, 1, "djvudocument"); + unsigned long size = ddjvu_cache_get_size(doc->context); + printf("## ddjvu_cache_get_size = %d\n", size); + lua_pushnumber(L, size); + return 1; +} + +static int cleanCache(lua_State *L) { + DjvuDocument *doc = (DjvuDocument*) luaL_checkudata(L, 1, "djvudocument"); + printf("## ddjvu_cache_clear\n"); + ddjvu_cache_clear(doc->context); + return 0; +} + static const struct luaL_Reg djvu_func[] = { {"openDocument", openDocument}, {NULL, NULL} @@ -333,8 +473,11 @@ static const struct luaL_Reg djvu_func[] = { static const struct luaL_Reg djvudocument_meth[] = { {"openPage", openPage}, {"getPages", getNumberOfPages}, - {"getTOC", getTableOfContent}, + {"getToc", getTableOfContent}, + {"getPageText", getPageText}, {"close", closeDocument}, + {"getCacheSize", getCacheSize}, + {"cleanCache", cleanCache}, {"__gc", closeDocument}, {NULL, NULL} }; diff --git a/djvureader.lua b/djvureader.lua index 0582073b9..f9142c366 100644 --- a/djvureader.lua +++ b/djvureader.lua @@ -6,9 +6,81 @@ DJVUReader = UniReader:new{} -- DJVU does not support password yet function DJVUReader:open(filename) local ok - ok, self.doc = pcall(djvu.openDocument, filename) + ok, self.doc = pcall(djvu.openDocument, filename, self.cache_document_size) if not ok then return ok, self.doc -- this will be the error message instead end return ok end + +function DJVUReader:init() + self:addAllCommands() + self:adjustDjvuReaderCommand() +end + +function DJVUReader:adjustDjvuReaderCommand() + self.commands:del(KEY_J, MOD_SHIFT, "J") + self.commands:del(KEY_K, MOD_SHIFT, "K") +end + + +---------------------------------------------------- +-- highlight support +---------------------------------------------------- +function DJVUReader:getText(pageno) + return self.doc:getPageText(pageno) +end + +---------------------------------------------------- +-- In djvulibre library, some coordinates starts from +-- lower left conner, i.e. y is upside down in kpv's +-- coordinate. So y0 should be taken with special care. +---------------------------------------------------- +function DJVUReader:zoomedRectCoordTransform(x0, y0, x1, y1) + local x,y = self:screenOffset() + return + x0 * self.globalzoom + x, + self.cur_full_height - (y1 * self.globalzoom) + y, + (x1 - x0) * self.globalzoom, + (y1 - y0) * self.globalzoom +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 + G_height) +end + +-- y axel in djvulibre starts from bottom +function DJVUReader:_isEntireLineInScreenHeightRange(l) + return (l ~= nil) and + (self.cur_full_height - (l.y1 * self.globalzoom) >= + -self.offset_y) and + (self.cur_full_height - (l.y0 * self.globalzoom) <= + -self.offset_y + G_height) +end + +-- y axel in djvulibre starts from bottom +function DJVUReader:_isWordInScreenRange(w) + if not w then + return false + end + + is_entire_word_out_of_screen_height = + (self.cur_full_height - (w.y0 * self.globalzoom) <= + -self.offset_y) + or (self.cur_full_height - (w.y1 * self.globalzoom) >= + -self.offset_y + G_height) + + is_entire_word_out_of_screen_width = + (w.x0 * self.globalzoom >= -self.offset_x + G_width + or w.x1 * self.globalzoom <= -self.offset_x) + + return (not is_entire_word_out_of_screen_height) and + (not is_entire_word_out_of_screen_width) +end + + diff --git a/einkfb.c b/einkfb.c index a1f7054f9..cfc268719 100644 --- a/einkfb.c +++ b/einkfb.c @@ -150,6 +150,16 @@ static int einkUpdate(lua_State *L) { ioctl(fb->fd, FBIO_EINK_UPDATE_DISPLAY_AREA, &myarea); #else // for now, we only do fullscreen blits in emulation mode + if (fxtype == 0) { + // simmulate a full screen update in eink screen + if(SDL_MUSTLOCK(fb->screen) && (SDL_LockSurface(fb->screen) < 0)) { + return luaL_error(L, "can't lock surface."); + } + SDL_FillRect(fb->screen, NULL, 0x000000); + if(SDL_MUSTLOCK(fb->screen)) SDL_UnlockSurface(fb->screen); + SDL_Flip(fb->screen); + } + if(SDL_MUSTLOCK(fb->screen) && (SDL_LockSurface(fb->screen) < 0)) { return luaL_error(L, "can't lock surface."); } diff --git a/filechooser.lua b/filechooser.lua index c4e2f1378..82f821fbc 100644 --- a/filechooser.lua +++ b/filechooser.lua @@ -54,7 +54,11 @@ function FileChooser:readDir() table.insert(self.dirs, f) else local file_type = string.lower(string.match(f, ".+%.([^.]+)") or "") - if file_type == "djvu" or file_type == "pdf" or file_type == "xps" or file_type == "cbz" then + if file_type == "djvu" + or file_type == "pdf" or file_type == "xps" or file_type == "cbz" + or file_type == "epub" or file_type == "txt" or file_type == "rtf" + or file_type == "htm" or file_type == "html" + or file_type == "fb2" or file_type == "chm" then table.insert(self.files, f) end end @@ -118,8 +122,8 @@ function FileChooser:choose(ypos, height) end while true do - local cface, cfhash= Font:getFaceAndHash(25) - local fface, ffhash = Font:getFaceAndHash(16, Font.ffont) + local cface = Font:getFace("cfont", 25) + local fface = Font:getFace("ffont", 16) if pagedirty then fb.bb:paintRect(0, ypos, fb.bb:getWidth(), height, 0) @@ -128,17 +132,17 @@ function FileChooser:choose(ypos, height) local i = (self.page - 1) * perpage + c if i <= #self.dirs then -- resembles display in midnight commander: adds "/" prefix for directories - renderUtf8Text(fb.bb, 39, ypos + self.spacing*c, cface, cfhash, "/", true) - renderUtf8Text(fb.bb, 50, ypos + self.spacing*c, cface, cfhash, self.dirs[i], true) + renderUtf8Text(fb.bb, 39, ypos + self.spacing*c, cface, "/", true) + renderUtf8Text(fb.bb, 50, ypos + self.spacing*c, cface, self.dirs[i], true) elseif i <= self.items then - renderUtf8Text(fb.bb, 50, ypos + self.spacing*c, cface, cfhash, self.files[i-#self.dirs], true) + renderUtf8Text(fb.bb, 50, ypos + self.spacing*c, cface, self.files[i-#self.dirs], true) end end - renderUtf8Text(fb.bb, 5, ypos + self.spacing * perpage + 42, fface, ffhash, + renderUtf8Text(fb.bb, 5, ypos + self.spacing * perpage + 42, fface, "Page "..self.page.." of "..(math.floor(self.items / perpage)+1), true) local msg = self.exception_message and self.exception_message:match("[^%:]+:%d+: (.*)") or "Path: "..self.path self.exception_message = nil - renderUtf8Text(fb.bb, 5, ypos + self.spacing * (perpage+1) + 27, fface, ffhash, msg, true) + renderUtf8Text(fb.bb, 5, ypos + self.spacing * (perpage+1) + 27, fface, msg, true) markerdirty = true end if markerdirty then @@ -160,7 +164,7 @@ function FileChooser:choose(ypos, height) pagedirty = false end - local ev = input.waitForEvent() + local ev = input.saveWaitForEvent() --print("key code:"..ev.code) ev.code = adjustKeyEvents(ev) if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then @@ -169,13 +173,13 @@ function FileChooser:choose(ypos, height) elseif ev.code == KEY_FW_DOWN then nextItem() elseif ev.code == KEY_F then -- invoke fontchooser menu - fonts_menu = SelectMenu:new{ + local fonts_menu = SelectMenu:new{ menu_title = "Fonts Menu", - item_array = Font.fonts, + item_array = Font:getFontList(), } - local re = fonts_menu:choose(0, height) + local re, font = fonts_menu:choose(0, height) if re then - Font.cfont = Font.fonts[re] + Font.fontmap["cfont"] = font Font:update() end pagedirty = true @@ -193,11 +197,11 @@ function FileChooser:choose(ypos, height) --]] return nil, function() FileSearcher:init( self.path ) - FileSearcher:choose(ypos, height, keywords) + FileSearcher:choose(keywords) 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 +212,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/filesearcher.lua b/filesearcher.lua index f2792c268..c0426029c 100644 --- a/filesearcher.lua +++ b/filesearcher.lua @@ -30,10 +30,16 @@ function FileSearcher:readDir() for __, d in pairs(self.dirs) do -- handle files in d for f in lfs.dir(d) do + local file_type = string.lower(string.match(f, ".+%.([^.]+)") or "") if lfs.attributes(d.."/"..f, "mode") == "directory" and f ~= "." and f~= ".." and not string.match(f, "^%.[^.]") then table.insert(new_dirs, d.."/"..f) - elseif string.match(f, ".+%.[pP][dD][fF]$") or string.match(f, ".+%.[dD][jJ][vV][uU]$") then + elseif file_type == "djvu" or file_type == "pdf" + or file_type == "xps" or file_type == "cbz" + or file_type == "epub" or file_type == "txt" + or file_type == "rtf" or file_type == "htm" + or file_type == "html" + or file_type == "fb2" or file_type == "chm" then file_entry = {dir=d, name=f,} table.insert(self.files, file_entry) --print("file:"..d.."/"..f) @@ -68,6 +74,7 @@ function FileSearcher:setSearchResult(keywords) end end end + self.keywords = keywords self.items = #self.result self.page = 1 self.current = 1 @@ -79,42 +86,138 @@ function FileSearcher:init(search_path) else self:setPath("/mnt/us/documents") end + self:addAllCommands() end +function FileSearcher:prevItem() + if self.current == 1 then + if self.page > 1 then + self.current = self.perpage + self.page = self.page - 1 + self.pagedirty = true + end + else + self.current = self.current - 1 + self.markerdirty = true + end +end -function FileSearcher:choose(ypos, height, keywords) - local perpage = math.floor(height / self.spacing) - 2 - local pagedirty = true - local markerdirty = false - - local prevItem = function () - if self.current == 1 then - if self.page > 1 then - self.current = perpage - self.page = self.page - 1 - pagedirty = true - end - else - self.current = self.current - 1 - markerdirty = true +function FileSearcher:nextItem() + if self.current == self.perpage then + if self.page < (self.items / self.perpage) then + self.current = 1 + self.page = self.page + 1 + self.pagedirty = true + end + else + if self.page ~= math.floor(self.items / self.perpage) + 1 + or self.current + (self.page-1)*self.perpage < self.items then + self.current = self.current + 1 + self.markerdirty = true end end +end - local nextItem = function () - if self.current == perpage then - if self.page < (self.items / perpage) then - self.current = 1 +function FileSearcher:addAllCommands() + self.commands = Commands:new{} + + self.commands:add(KEY_FW_UP, nil, "joypad up", + "goto previous item", + function(self) + self:prevItem() + end + ) + self.commands:add(KEY_FW_DOWN, nil, "joypad down", + "goto next item", + function(self) + self:nextItem() + end + ) + self.commands:add(KEY_PGFWD, nil, ">", + "next page", + function(self) + if self.page < (self.items / self.perpage) then + if self.current + self.page*self.perpage > self.items then + self.current = self.items - self.page*self.perpage + end self.page = self.page + 1 - pagedirty = true - end - else - if self.page ~= math.floor(self.items / perpage) + 1 - or self.current + (self.page-1)*perpage < self.items then - self.current = self.current + 1 - markerdirty = true + self.pagedirty = true + else + self.current = self.items - (self.page-1)*self.perpage + self.markerdirty = true end end - end + ) + self.commands:add(KEY_PGBCK, nil, "<", + "previous page", + function(self) + if self.page > 1 then + self.page = self.page - 1 + self.pagedirty = true + else + self.current = 1 + self.markerdirty = true + end + end + ) + self.commands:add(KEY_S, nil, "S", + "invoke search inputbox", + function(self) + old_keywords = self.keywords + self.keywords = InputBox:input(G_height - 100, 100, + "Search:", old_keywords) + if self.keywords then + self:setSearchResult(self.keywords) + else + self.keywords = old_keywords + end + self.pagedirty = true + end + ) + self.commands:add(KEY_F, nil, "F", + "font menu", + function(self) + local fonts_menu = SelectMenu:new{ + menu_title = "Fonts Menu", + item_array = Font:getFontList(), + } + local re, font = fonts_menu:choose(0, G_height) + if re then + Font.fontmap["cfont"] = font + Font:update() + end + self.pagedirty = true + end + ) + self.commands:add({KEY_ENTER, KEY_FW_PRESS}, nil, "", + "select item", + function(self) + file_entry = self.result[self.perpage*(self.page-1)+self.current] + file_full_path = file_entry.dir .. "/" .. file_entry.name + + openFile(file_full_path) + --reset height and item index if screen has been rotated + local item_no = self.perpage * (self.page - 1) + self.current + self.perpage = math.floor(G_height / self.spacing) - 2 + self.current = item_no % self.perpage + self.page = math.floor(item_no / self.perpage) + 1 + + self.pagedirty = true + end + ) + self.commands:add({KEY_BACK, KEY_HOME}, nil, "", + "back to file browser", + function(self) + return "break" + end + ) +end + +function FileSearcher:choose(keywords) + self.perpage = math.floor(G_height / self.spacing) - 2 + self.pagedirty = true + self.markerdirty = false + -- if given keywords, set new result according to keywords. -- Otherwise, display the previous search result. @@ -123,136 +226,92 @@ function FileSearcher:choose(ypos, height, keywords) end while true do - local cface, cfhash = Font:getFaceAndHash(22) - local tface, tfhash = Font:getFaceAndHash(25, Font.tfont) - local fface, ffhash = Font:getFaceAndHash(16, Font.ffont) + local cface = Font:getFace("cfont", 22) + local tface = Font:getFace("tfont", 25) + local fface = Font:getFace("ffont", 16) - if pagedirty then - markerdirty = true - fb.bb:paintRect(0, ypos, fb.bb:getWidth(), height, 0) + if self.pagedirty then + self.markerdirty = true + fb.bb:paintRect(0, 0, G_width, G_height, 0) -- draw menu title - renderUtf8Text(fb.bb, 30, ypos + self.title_H, tface, tfhash, - "Search Result for: "..keywords, true) + renderUtf8Text(fb.bb, 30, 0 + self.title_H, tface, + "Search Result for: "..self.keywords, true) -- draw results local c if self.items == 0 then -- nothing found - y = ypos + self.title_H + self.spacing * 2 - renderUtf8Text(fb.bb, 20, y, cface, cfhash, + y = self.title_H + self.spacing * 2 + renderUtf8Text(fb.bb, 20, y, cface, "Sorry, no match found.", true) - renderUtf8Text(fb.bb, 20, y + self.spacing, cface, cfhash, + renderUtf8Text(fb.bb, 20, y + self.spacing, cface, "Please try a different keyword.", true) - markerdirty = false + self.markerdirty = false else -- found something, draw it - for c = 1, perpage do - local i = (self.page - 1) * perpage + c + for c = 1, self.perpage do + local i = (self.page - 1) * self.perpage + c if i <= self.items then - y = ypos + self.title_H + (self.spacing * c) - renderUtf8Text(fb.bb, 50, y, cface, cfhash, + y = self.title_H + (self.spacing * c) + renderUtf8Text(fb.bb, 50, y, cface, self.result[i].name, true) end end end -- draw footer - y = ypos + self.title_H + (self.spacing * perpage) + self.foot_H - x = (fb.bb:getWidth() / 2) - 50 - all_page = (math.floor(self.items / perpage)+1) - renderUtf8Text(fb.bb, x, y, fface, ffhash, + y = self.title_H + (self.spacing * self.perpage) + self.foot_H + x = (G_width / 2) - 50 + all_page = (math.floor(self.items / self.perpage)+1) + renderUtf8Text(fb.bb, x, y, fface, "Page "..self.page.." of "..all_page, true) end - if markerdirty then - if not pagedirty then + if self.markerdirty then + if not self.pagedirty then if self.oldcurrent > 0 then - y = ypos + self.title_H + (self.spacing * self.oldcurrent) + 10 - fb.bb:paintRect(30, y, fb.bb:getWidth() - 60, 3, 0) - fb:refresh(1, 30, y, fb.bb:getWidth() - 60, 3) + y = self.title_H + (self.spacing * self.oldcurrent) + 10 + fb.bb:paintRect(30, y, G_width - 60, 3, 0) + fb:refresh(1, 30, y, G_width - 60, 3) end end -- draw new marker line - y = ypos + self.title_H + (self.spacing * self.current) + 10 - fb.bb:paintRect(30, y, fb.bb:getWidth() - 60, 3, 15) - if not pagedirty then - fb:refresh(1, 30, y, fb.bb:getWidth() - 60, 3) + y = self.title_H + (self.spacing * self.current) + 10 + fb.bb:paintRect(30, y, G_width - 60, 3, 15) + if not self.pagedirty then + fb:refresh(1, 30, y, G_width - 60, 3) end self.oldcurrent = self.current - markerdirty = false + self.markerdirty = false end - if pagedirty then - fb:refresh(0, 0, ypos, fb.bb:getWidth(), height) - pagedirty = false + if self.pagedirty then + fb:refresh(0) + self.pagedirty = false end - local ev = input.waitForEvent() + local ev = input.saveWaitForEvent() ev.code = adjustKeyEvents(ev) if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then - if ev.code == KEY_FW_UP then - prevItem() - elseif ev.code == KEY_FW_DOWN then - nextItem() - elseif ev.code == KEY_PGFWD then - if self.page < (self.items / perpage) then - if self.current + self.page*perpage > self.items then - self.current = self.items - self.page*perpage - end - self.page = self.page + 1 - pagedirty = true - else - self.current = self.items - (self.page-1)*perpage - markerdirty = true - end - elseif ev.code == KEY_PGBCK then - if self.page > 1 then - self.page = self.page - 1 - pagedirty = true - else - self.current = 1 - markerdirty = true - end - elseif ev.code == KEY_S then - old_keywords = keywords - keywords = InputBox:input(height-100, 100, "Search:", old_keywords) - if keywords then - self:setSearchResult(keywords) - else - keywords = old_keywords - end - pagedirty = true - elseif ev.code == KEY_F then -- invoke fontchooser menu - fonts_menu = SelectMenu:new{ - menu_title = "Fonts Menu", - item_array = Font.fonts, - } - local re = fonts_menu:choose(0, height) - if re then - Font.cfont = Font.fonts[re] - Font:update() - end - pagedirty = true - elseif ev.code == KEY_ENTER or ev.code == KEY_FW_PRESS then - file_entry = self.result[perpage*(self.page-1)+self.current] - file_full_path = file_entry.dir .. "/" .. file_entry.name + keydef = Keydef:new(ev.code, getKeyModifier()) + print("key pressed: "..tostring(keydef)) - -- rotation mode might be changed while reading, so - -- record height_percent here - local height_percent = height/fb.bb:getHeight() - openFile(file_full_path) - - --reset height and item index if screen has been rotated - local old_perpage = perpage - height = math.floor(fb.bb:getHeight()*height_percent) - perpage = math.floor(height / self.spacing) - 2 - self.current = (old_perpage * (self.page - 1) + - self.current) % perpage - self.page = math.floor(self.items / perpage) + 1 - - pagedirty = true - elseif ev.code == KEY_BACK or ev.code == KEY_HOME then - return nil + command = self.commands:getByKeydef(keydef) + if command ~= nil then + print("command to execute: "..tostring(command)) + ret_code = command.func(self, keydef) + else + print("command not found: "..tostring(command)) end - end - end + + if ret_code == "break" then + break + end + + if self.selected_item ~= nil then + print("# selected "..self.selected_item) + return self.selected_item + end + end -- if + end -- while true + return nil end diff --git a/font.lua b/font.lua index ef31d6277..cdf658956 100644 --- a/font.lua +++ b/font.lua @@ -1,23 +1,43 @@ Font = { - -- default font for menu contents - cfont = "sans", - -- default font for title - tfont = "Helvetica-BoldOblique", - -- default font for footer - ffont = "sans", + fontmap = { + -- default font for menu contents + cfont = "droid/DroidSans.ttf", + -- default font for title + tfont = "NimbusSanL-BoldItal.cff", + -- default font for footer + ffont = "droid/DroidSans.ttf", - -- built in fonts - fonts = {"sans", "cjk", "mono", - "Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique", - "Helvetica", "Helvetica-Oblique", "Helvetica-BoldOblique", - "Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic",}, + -- default font for reading position info + rifont = "droid/DroidSans.ttf", + + -- default font for pagination display + pgfont = "droid/DroidSans.ttf", + + -- selectmenu: font for item shortcut + scfont = "droid/DroidSansMono.ttf", + + -- help page: font for displaying keys + hpkfont = "droid/DroidSansMono.ttf", + -- font for displaying help messages + hfont = "droid/DroidSans.ttf", + + -- font for displaying input content + -- we have to use mono here for better distance controlling + infont = "droid/DroidSansMono.ttf", + + -- font for info messages + infofont = "droid/DroidSans.ttf", + }, + + fontdir = os.getenv("FONTDIR") or "./fonts", -- face table faces = {}, } -function Font:getFaceAndHash(size, font) + +function Font:getFace(font, size) if not font then -- default to content font font = self.cfont @@ -26,21 +46,45 @@ function Font:getFaceAndHash(size, font) local face = self.faces[font..size] -- build face if not found if not face then - for _k,_v in ipairs(self.fonts) do - if font == _v then - face = freetype.newBuiltinFace(font, size) - self.faces[font..size] = face - end + local realname = self.fontmap[font] + if not realname then + realname = font end - if not face then - print("#! Font "..font.." not supported!!") + realname = self.fontdir.."/"..realname + ok, face = pcall(freetype.newFace, realname, size) + if not ok then + print("#! Font "..font.." ("..realname..") not supported: "..face) return nil end + self.faces[font..size] = face end - return face, font..size + return { size = size, ftface = face, hash = font..size } +end + +function Font:_readList(target, dir, effective_dir) + for f in lfs.dir(dir) do + if lfs.attributes(dir.."/"..f, "mode") == "directory" and f ~= "." and f ~= ".." then + self:_readList(target, dir.."/"..f, effective_dir..f.."/") + else + local file_type = string.lower(string.match(f, ".+%.([^.]+)") or "") + if file_type == "ttf" or file_type == "cff" or file_type == "otf" then + table.insert(target, effective_dir..f) + end + end + end +end + +function Font:getFontList() + fontlist = {} + self:_readList(fontlist, self.fontdir, "") + table.sort(fontlist) + return fontlist end function Font:update() + for _k, _v in ipairs(self.faces) do + _v:done() + end self.faces = {} clearGlyphCache() end diff --git a/ft.c b/ft.c index a073fd415..6d4304913 100644 --- a/ft.c +++ b/ft.c @@ -55,49 +55,6 @@ static int newFace(lua_State *L) { return 1; } -static int newBuiltinFace(lua_State *L) { - const char *fontname = luaL_checkstring(L, 1); - int pxsize = luaL_optint(L, 2, 16*64); - char *fontdata = NULL; - - unsigned int size; - /* we use compiled-in font data from mupdf build */ - if(!strcmp("mono", fontname)) { - fontdata = pdf_lookup_substitute_font(1, 0, 0, 0, &size); - } else if(!strcmp("sans", fontname)) { - fontdata = pdf_lookup_substitute_font(0, 0, 0, 0, &size); - } else if(!strcmp("cjk", fontname)) { - fontdata = pdf_lookup_substitute_cjk_font(0, 0, &size); - } else { - fontdata = pdf_lookup_builtin_font(fontname, &size); - } - if(fontdata == NULL) { - return luaL_error(L, "no such built-in font"); - } - - FT_Face *face = (FT_Face*) lua_newuserdata(L, sizeof(FT_Face)); - luaL_getmetatable(L, "ft_face"); - lua_setmetatable(L, -2); - - FT_Error error = FT_New_Memory_Face(freetypelib, (FT_Byte*)fontdata, size, 0, face); - if(error) { - return luaL_error(L, "freetype error"); - } - - error = FT_Set_Pixel_Sizes(*face, 0, pxsize); - if(error) { - error = FT_Done_Face(*face); - return luaL_error(L, "freetype error"); - } - - if((*face)->charmap == NULL) { - //TODO - //fprintf(stderr, "no unicode charmap found, to be implemented.\n"); - } - return 1; -} - - static int renderGlyph(lua_State *L) { FT_Face *face = (FT_Face*) luaL_checkudata(L, 1, "ft_face"); int ch = luaL_checkint(L, 2); @@ -215,7 +172,6 @@ static const struct luaL_Reg ft_face_meth[] = { static const struct luaL_Reg ft_func[] = { {"newFace", newFace}, - {"newBuiltinFace", newBuiltinFace}, {NULL, NULL} }; 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/helppage.lua b/helppage.lua index 12fba6ff5..5c7041bd3 100644 --- a/helppage.lua +++ b/helppage.lua @@ -7,22 +7,33 @@ require "selectmenu" require "commands" HelpPage = { + -- Other Class vars: + + -- spacing between lines + spacing = 25, + -- state buffer commands = nil, items = 0, - page = 1 + page = 1, + + -- font for displaying keys + fsize = 20, + face = Font:getFace("hpkfont", 20), + + -- font for displaying help messages + hfsize = 20, + hface = Font:getFace("hfont", 20), + + -- font for paging display + ffsize = 15, + fface = Font:getFace("pgfont", 15) } -- Other Class vars: --- font for displaying help messages -HelpPage.sFace, HelpPage.sHash = Font:getFaceAndHash(20, "sans") --- font for displaying keys -HelpPage.mFace, HelpPage.mHash = Font:getFaceAndHash(20, "sans") --- font for paging display -HelpPage.fFace, HelpPage.fHash = Font:getFaceAndHash(15, "sans") -function HelpPage:show(ypos,height,commands) +function HelpPage:show(ypos, height, commands) self.commands = {} self.items = 0 local keys = {} @@ -37,17 +48,17 @@ function HelpPage:show(ypos,height,commands) end table.sort(self.commands,function(w1,w2) return w1.order #include #include +#include #include #include "input.h" +#include +#include +#include -#define NUM_FDS 3 -int inputfds[3] = { -1, -1, -1 }; +#define OUTPUT_SIZE 21 +#define CODE_IN_SAVER 10000 +#define CODE_OUT_SAVER 10001 + +#define NUM_FDS 4 +int inputfds[4] = { -1, -1, -1, -1 }; +int slider_pid = -1; + +int findFreeFdSlot() { + int i; + for(i=0; i: %d", inputdevice, errno); + fd = findFreeFdSlot(); + if(fd == -1) { + return luaL_error(L, "no free slot for new input device <%s>", inputdevice); + } + + if(!strcmp("slider",inputdevice)) { + /* special case: the power slider */ + int pipefd[2]; + int childpid; + + pipe(pipefd); + if((childpid = fork()) == -1) { + return luaL_error(L, "cannot fork() slider event listener"); + } + if(childpid == 0) { + FILE *fp; + char std_out[OUTPUT_SIZE] = ""; + struct input_event ev; + int ret; + __u16 key_code = 10000; + + close(pipefd[0]); + + ev.type = EV_KEY; + ev.code = key_code; + ev.value = 1; + + /* listen power slider events */ + while(1) { + fp = popen("exec lipc-wait-event com.lab126.powerd goingToScreenSaver,outOfScreenSaver", "r"); + if(fgets(std_out, OUTPUT_SIZE, fp) == NULL) { + break; + } + pclose(fp); + if(std_out[0] == 'g') { + ev.code = CODE_IN_SAVER; + } else if(std_out[0] == 'o') { + ev.code = CODE_OUT_SAVER; + } else { + printf("Unrecognized event.\n"); + } + /* fill event struct */ + gettimeofday(&ev.time, NULL); + + /* generate event */ + if(write(pipefd[1], &ev, sizeof(struct input_event)) == -1) { + break; + } } + exit(0); /* cannot be reached?! */ + } else { + close(pipefd[1]); + inputfds[fd] = pipefd[0]; + slider_pid = childpid; + } + } else { + inputfds[fd] = open(inputdevice, O_RDONLY | O_NONBLOCK, 0); + if(inputfds[fd] != -1) { + ioctl(inputfds[fd], EVIOCGRAB, 1); + return 0; + } else { + return luaL_error(L, "error opening input device <%s>: %d", inputdevice, errno); } } - return luaL_error(L, "no free slot for new input device <%s>", inputdevice); #else if(SDL_Init(SDL_INIT_VIDEO) < 0) { return luaL_error(L, "cannot initialize SDL."); @@ -58,6 +129,11 @@ static int closeInputDevices(lua_State *L) { close(i); } } + if(slider_pid != -1) { + /* kill and wait for child process */ + kill(slider_pid, SIGTERM); + waitpid(-1, NULL, 0); + } return 0; } @@ -66,7 +142,7 @@ static int waitForInput(lua_State *L) { fd_set fds; struct timeval timeout; int i, num, nfds; - int usecs = luaL_optint(L, 1, 0x7FFFFFFF); + int usecs = luaL_optint(L, 1, -1); // we check for <0 later timeout.tv_sec = (usecs/1000000); timeout.tv_usec = (usecs%1000000); @@ -81,7 +157,11 @@ static int waitForInput(lua_State *L) { nfds = inputfds[i] + 1; } - num = select(nfds, &fds, NULL, NULL, &timeout); + /* when no value is given as argument, we pass + * NULL to select() for the timeout value, setting no + * timeout at all. + */ + num = select(nfds, &fds, NULL, NULL, (usecs < 0) ? NULL : &timeout); if(num < 0) { return luaL_error(L, "Waiting for input failed: %d\n", errno); } diff --git a/inputbox.lua b/inputbox.lua index 2f354610e..7359f3ef5 100644 --- a/inputbox.lua +++ b/inputbox.lua @@ -1,9 +1,17 @@ +require "font" require "rendertext" require "keys" require "graphics" + +---------------------------------------------------- +-- General inputbox +---------------------------------------------------- + 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 +23,103 @@ InputBox = { shiftmode = false, altmode = false, + cursor = nil, + -- font for displaying input content - face = freetype.newBuiltinFace("mono", 25), - fhash = "m25", + -- we have to use mono here for better distance controlling + face = Font:getFace("infont", 25), fheight = 25, - fwidth = 16, + fwidth = 15, + commands = nil, + initialized = false, } -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 +function InputBox:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o end -function InputBox:addString(str) - for i = 1, #str do - self:addChar(str:sub(i,i)) +function InputBox:init() + if not self.initialized then + self:addAllCommands() + self.initialized = true end 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.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(1, 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:drawHelpMsg(ypos, w, h) + return end function InputBox:drawBox(ypos, w, h, title) @@ -61,139 +128,262 @@ function InputBox:drawBox(ypos, w, h, title) -- draw input slot fb.bb:paintRect(140, ypos + 10, w - 130, h - 20, self.input_bg) -- draw input title - renderUtf8Text(fb.bb, 35, self.input_start_y, self.face, self.fhash, + renderUtf8Text(fb.bb, 35, self.input_start_y, self.face, title, true) 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 + self:init() -- do some initilization + self.ypos = ypos + 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:drawHelpMsg(ypos, w, h) + 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() + local ev = input.saveWaitForEvent() ev.code = adjustKeyEvents(ev) if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then - --local secs, usecs = util.gettime() - if ev.code == KEY_FW_UP then - elseif ev.code == KEY_FW_DOWN then - elseif ev.code == KEY_A then - self:addChar("a") - elseif ev.code == KEY_B then - self:addChar("b") - elseif ev.code == KEY_C then - self:addChar("c") - elseif ev.code == KEY_D then - self:addChar("d") - elseif ev.code == KEY_E then - self:addChar("e") - elseif ev.code == KEY_F then - self:addChar("f") - elseif ev.code == KEY_G then - self:addChar("g") - elseif ev.code == KEY_H then - self:addChar("h") - elseif ev.code == KEY_I then - self:addChar("i") - elseif ev.code == KEY_J then - self:addChar("j") - elseif ev.code == KEY_K then - self:addChar("k") - elseif ev.code == KEY_L then - self:addChar("l") - elseif ev.code == KEY_M then - self:addChar("m") - elseif ev.code == KEY_N then - self:addChar("n") - elseif ev.code == KEY_O then - self:addChar("o") - elseif ev.code == KEY_P then - self:addChar("p") - elseif ev.code == KEY_Q then - self:addChar("q") - elseif ev.code == KEY_R then - self:addChar("r") - elseif ev.code == KEY_S then - self:addChar("s") - elseif ev.code == KEY_T then - self:addChar("t") - elseif ev.code == KEY_U then - self:addChar("u") - elseif ev.code == KEY_V then - self:addChar("v") - elseif ev.code == KEY_W then - self:addChar("w") - elseif ev.code == KEY_X then - self:addChar("x") - elseif ev.code == KEY_Y then - self:addChar("y") - elseif ev.code == KEY_Z then - self:addChar("z") - elseif ev.code == KEY_1 then - self:addChar("1") - elseif ev.code == KEY_2 then - self:addChar("2") - elseif ev.code == KEY_3 then - self:addChar("3") - elseif ev.code == KEY_4 then - self:addChar("4") - elseif ev.code == KEY_5 then - self:addChar("5") - elseif ev.code == KEY_6 then - self:addChar("6") - elseif ev.code == KEY_7 then - self:addChar("7") - elseif ev.code == KEY_8 then - self:addChar("8") - elseif ev.code == KEY_9 then - self:addChar("9") - elseif ev.code == KEY_0 then - self:addChar("0") - elseif ev.code == KEY_SPACE then - self:addChar(" ") - elseif ev.code == KEY_PGFWD then - elseif ev.code == KEY_PGBCK then - 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 - self.input_string = nil - break + keydef = Keydef:new(ev.code, getKeyModifier()) + print("key pressed: "..tostring(keydef)) + + command = self.commands:getByKeydef(keydef) + if command ~= nil then + print("command to execute: "..tostring(command)) + ret_code = command.func(self, keydef) + else + print("command not found: "..tostring(command)) end - --local nsecs, nusecs = util.gettime() - --local dur = (nsecs - secs) * 1000000 + nusecs - usecs - --print("E: T="..ev.type.." V="..ev.value.." C="..ev.code.." DUR="..dur) + if ret_code == "break" then + ret_code = nil + break + end end -- if end -- while - return self.input_string + local return_str = self.input_string + self.input_string = "" + return return_str +end + +function InputBox:addAllCommands() + if self.commands then + -- we only initialize once + return + end + self.commands = Commands:new{} + + INPUT_KEYS = { + {KEY_Q, "q"}, {KEY_W, "w"}, {KEY_E, "e"}, {KEY_R, "r"}, {KEY_T, "t"}, + {KEY_Y, "y"}, {KEY_U, "u"}, {KEY_I, "i"}, {KEY_O, "o"}, {KEY_P, "p"}, + + {KEY_A, "a"}, {KEY_S, "s"}, {KEY_D, "d"}, {KEY_F, "f"}, {KEY_G, "g"}, + {KEY_H, "h"}, {KEY_J, "j"}, {KEY_K, "k"}, {KEY_L, "l"}, + + {KEY_Z, "z"}, {KEY_X, "x"}, {KEY_C, "c"}, {KEY_V, "v"}, {KEY_B, "b"}, + {KEY_N, "n"}, {KEY_M, "m"}, + + {KEY_1, "1"}, {KEY_2, "2"}, {KEY_3, "3"}, {KEY_4, "4"}, {KEY_5, "5"}, + {KEY_6, "6"}, {KEY_7, "7"}, {KEY_8, "8"}, {KEY_9, "9"}, {KEY_0, "0"}, + + {KEY_SPACE, " "}, + + -- DXG keys + {KEY_DOT, "."}, {KEY_SLASH, "/"}, + } + for k,v in ipairs(INPUT_KEYS) do + self.commands:add(v[1], nil, "", + "input "..v[2], + function(self) + self:addChar(v[2]) + end + ) + end + + self.commands:add(KEY_FW_LEFT, nil, "", + "move cursor left", + function(self) + if (self.cursor.x_pos + 3) > self.input_start_x then + self.cursor:moveHorizontalAndDraw(-self.fwidth) + fb:refresh(1, self.input_start_x-5, self.ypos, + self.input_slot_w, self.h) + end + end + ) + self.commands:add(KEY_FW_RIGHT, nil, "", + "move cursor right", + function(self) + if (self.cursor.x_pos + 3) < self.input_cur_x then + self.cursor:moveHorizontalAndDraw(self.fwidth) + fb:refresh(1, self.input_start_x-5, self.ypos, + self.input_slot_w, self.h) + end + end + ) + self.commands:add({KEY_ENTER, KEY_FW_PRESS}, nil, "", + "submit input content", + function(self) + if self.input_string == "" then + self.input_string = nil + end + return "break" + end + ) + self.commands:add(KEY_DEL, nil, "", + "delete one character", + function(self) + self:delChar() + end + ) + self.commands:add(KEY_DEL, MOD_SHIFT, "", + "empty inputbox", + function(self) + self:clearText() + end + ) + self.commands:add({KEY_BACK, KEY_HOME}, nil, "", + "cancel inputbox", + function(self) + self.input_string = nil + return "break" + end + ) +end + + +---------------------------------------------------- +-- Inputbox for numbers only +-- Designed by eLiNK +---------------------------------------------------- + +NumInputBox = InputBox:new{ + initialized = false, + commands = Commands:new{}, +} + +function NumInputBox:addAllCommands() + self.commands = Commands:new{} + + INPUT_NUM_KEYS = { + {KEY_Q, "1"}, {KEY_W, "2"}, {KEY_E, "3"}, {KEY_R, "4"}, {KEY_T, "5"}, + {KEY_Y, "6"}, {KEY_U, "7"}, {KEY_I, "8"}, {KEY_O, "9"}, {KEY_P, "0"}, + + {KEY_1, "1"}, {KEY_2, "2"}, {KEY_3, "3"}, {KEY_4, "4"}, {KEY_5, "5"}, + {KEY_6, "6"}, {KEY_7, "7"}, {KEY_8, "8"}, {KEY_9, "9"}, {KEY_0, "0"}, + } + for k,v in ipairs(INPUT_NUM_KEYS) do + self.commands:add(v[1], nil, "", + "input "..v[2], + function(self) + self:addChar(v[2]) + end + ) + end -- for + + self.commands:add(KEY_FW_LEFT, nil, "", + "move cursor left", + function(self) + if (self.cursor.x_pos + 3) > self.input_start_x then + self.cursor:moveHorizontalAndDraw(-self.fwidth) + fb:refresh(1, self.input_start_x-5, self.ypos, + self.input_slot_w, self.h) + end + end + ) + self.commands:add(KEY_FW_RIGHT, nil, "", + "move cursor right", + function(self) + if (self.cursor.x_pos + 3) < self.input_cur_x then + self.cursor:moveHorizontalAndDraw(self.fwidth) + fb:refresh(1, self.input_start_x-5, self.ypos, + self.input_slot_w, self.h) + end + end + ) + self.commands:add({KEY_ENTER, KEY_FW_PRESS}, nil, "", + "submit input content", + function(self) + if self.input_string == "" then + self.input_string = nil + end + return "break" + end + ) + self.commands:add(KEY_DEL, nil, "", + "delete one character", + function(self) + self:delChar() + end + ) + self.commands:add(KEY_DEL, MOD_SHIFT, "", + "empty inputbox", + function(self) + self:clearText() + end + ) + self.commands:add({KEY_BACK, KEY_HOME}, nil, "", + "cancel inputbox", + function(self) + self.input_string = nil + return "break" + end + ) +end + +function NumInputBox:drawHelpMsg(ypos, w, h) + local w = 415 + local y = ypos - 60 + local x = (G_width - w) / 2 + local h = 50 + local bw = 2 + local face = Font:getFace("scfont", 22) + + fb.bb:paintRect(x, y, w, h, 15) + fb.bb:paintRect(x+bw, y+bw, w-2*bw, h-2*bw, 0) + + local font_y = y + 22 + local font_x = x + 22 + INPUT_NUM_KEYS = { + {"Q", "1"}, {"W", "2"}, {"E", "3"}, {"R", "4"}, {"T", "5"}, + {"Y", "6"}, {"U", "7"}, {"I", "8"}, {"O", "9"}, {"P", "0"}, + } + for k,v in ipairs(INPUT_NUM_KEYS) do + renderUtf8Text(fb.bb, font_x, font_y, face, + v[1], true) + renderUtf8Text(fb.bb, font_x, font_y + 22, face, + v[2], true) + font_x = font_x + 40 + end + + fb:refresh(1, x, y, w, h) end diff --git a/keys.lua b/keys.lua index e07af2ada..14f41ee8a 100644 --- a/keys.lua +++ b/keys.lua @@ -87,6 +87,9 @@ KEY_FW_UP = 122 KEY_FW_DOWN = 123 KEY_FW_PRESS = 92 +KEY_INTO_SCREEN_SAVER = 10000 +KEY_OUTOF_SCREEN_SAVER = 10001 + -- constants from EV_KEY = 1 @@ -126,6 +129,8 @@ end function setEmuKeycodes() KEY_PGFWD = 117 KEY_PGBCK = 112 + KEY_LPGBCK = 69 -- F3 + KEY_LPGFWD = 70 -- F4 KEY_HOME = 110 -- home KEY_BACK = 22 -- backspace KEY_DEL = 119 -- Delete @@ -252,3 +257,22 @@ function adjustKeyEvents(ev) print("# Unrecognizable rotation mode "..Screen.cur_rotation_mode.."!") return nil end + +-- wrapper for input.waitForEvents that will retry for some cases +function input.saveWaitForEvent(timeout) + local retry = true + while retry do + local ok, ev = pcall(input.waitForEvent, timeout) + if not ok then + print("got error waiting for events:", ev) + if ev == "Waiting for input failed: 4\n" then + -- EINTR, we got interrupted. Try and restart + retry = true + else + retry = false + end + else + return ev + end + end +end diff --git a/kpdfview.c b/kpdfview.c index d14a38053..d0003ae7f 100644 --- a/kpdfview.c +++ b/kpdfview.c @@ -26,6 +26,9 @@ #include "blitbuffer.h" #include "drawcontext.h" #include "pdf.h" +#include "mupdfimg.h" +#include "djvu.h" +#include "cre.h" #include "einkfb.h" #include "input.h" #include "ft.h" @@ -53,9 +56,11 @@ int main(int argc, char **argv) { luaopen_einkfb(L); luaopen_pdf(L); luaopen_djvu(L); + luaopen_cre(L); luaopen_input(L); luaopen_util(L); luaopen_ft(L); + luaopen_mupdfimg(L); luaopen_lfs(L); diff --git a/kpvcrlib/CMakeLists.txt b/kpvcrlib/CMakeLists.txt new file mode 100644 index 000000000..f148b2c1c --- /dev/null +++ b/kpvcrlib/CMakeLists.txt @@ -0,0 +1,57 @@ + +PROJECT(kpvcrlib) +cmake_minimum_required(VERSION 2.6) + +SET(MUPDF_DIR ../mupdf) +SET(MUPDF_3RDPARTY_DIR ${MUPDF_DIR}/thirdparty) +SET(CR_3RDPARTY_DIR crengine/thirdparty) + +SET(CR3_PNG 1) +#SET(CR3_JPEG 1) + +SET(FREETYPE_INCLUDE_DIRS ${MUPDF_3RDPARTY_DIR}/freetype-2.4.8/include) +#SET(FREETYPE_INCLUDE_DIRS ${CR_3RDPARTY_DIR}/freetype/include) +SET(ANTIWORD_INCLUDE_DIR ${CR_3RDPARTY_DIR}/antiword) +SET(CHM_INCLUDE_DIRS ${CR_3RDPARTY_DIR}/chmlib) +SET(PNG_INCLUDE_DIR ${CR_3RDPARTY_DIR}/libpng) +SET(ZLIB_INCLUDE_DIR ${MUPDF_3RDPARTY_DIR}/zlib-1.2.5) +#SET(ZLIB_INCLUDE_DIR ${CR_3RDPARTY_DIR}/zlib) +SET(JPEGLIB_INCLUDE_DIR ${MUPDF_3RDPARTY_DIR}/jpeg-8d) +#SET(JPEGLIB_INCLUDE_DIR ${CR_3RDPARTY_DIR}/libjpeg) +SET(JCONFIG_INCLUDE_DIR ${MUPDF_DIR}/scripts) + +INCLUDE_DIRECTORIES( + ${FREETYPE_INCLUDE_DIRS} + ${ANTIWORD_INCLUDE_DIR} + ${CHM_INCLUDE_DIRS} + ${PNG_INCLUDE_DIR} + ${ZLIB_INCLUDE_DIR} + ${JPEGLIB_INCLUDE_DIR} + ${JCONFIG_INCLUDE_DIR} +) + +ADD_DEFINITIONS(-DLINUX=1 -D_LINUX=1 -DUSE_FONTCONFIG=0 -DUSE_FREETYPE=1 -DCR3_PATCH=1 -DNDEBUG=1) + + +message("Will build patched LIBCHM library") +ADD_DEFINITIONS(-DCHM_SUPPORT_ENABLED=1) +ADD_SUBDIRECTORY(${CR_3RDPARTY_DIR}/chmlib) + +message("Will build patched LIBPNG library") +ADD_SUBDIRECTORY(${CR_3RDPARTY_DIR}/libpng) + +#message("Will build patched JPEGLIB library") +#ADD_SUBDIRECTORY(${CR_3RDPARTY_DIR}/libjpeg) + +message("Will not build patched ANTIWORD library, because we haven't supported dictionary lookup yet.") +#message("Will build patched ANTIWORD library") +ADD_DEFINITIONS(-DENABLE_ANTIWORD=0) +#ADD_DEFINITIONS(-DCR3_ANTIWORD_PATCH=1) +#ADD_SUBDIRECTORY(${CR_3RDPARTY_DIR}/antiword) + +message("Will build crengine library") +SET(GUI kpv) +#ADD_DEFINITIONS(-DJCONFIG_INCLUDED=1) +ADD_SUBDIRECTORY(crengine/crengine) + + diff --git a/kpvcrlib/crengine b/kpvcrlib/crengine new file mode 160000 index 000000000..ba469d334 --- /dev/null +++ b/kpvcrlib/crengine @@ -0,0 +1 @@ +Subproject commit ba469d33473670ca303e2ef7f9762452a86e18b1 diff --git a/kpvcrlib/jpeg_compress_struct_size.patch b/kpvcrlib/jpeg_compress_struct_size.patch new file mode 100644 index 000000000..e2da6de22 --- /dev/null +++ b/kpvcrlib/jpeg_compress_struct_size.patch @@ -0,0 +1,15 @@ +--- jcapimin.c 2012-04-04 00:02:30.000000000 +0800 ++++ jcapimin-patched.c 2012-04-04 00:02:26.000000000 +0800 +@@ -36,9 +36,9 @@ + cinfo->mem = NULL; /* so jpeg_destroy knows mem mgr not called */ + if (version != JPEG_LIB_VERSION) + ERREXIT2(cinfo, JERR_BAD_LIB_VERSION, JPEG_LIB_VERSION, version); +- if (structsize != SIZEOF(struct jpeg_compress_struct)) +- ERREXIT2(cinfo, JERR_BAD_STRUCT_SIZE, +- (int) SIZEOF(struct jpeg_compress_struct), (int) structsize); ++ /*if (structsize != SIZEOF(struct jpeg_compress_struct))*/ ++ /*ERREXIT2(cinfo, JERR_BAD_STRUCT_SIZE, */ ++ /*(int) SIZEOF(struct jpeg_compress_struct), (int) structsize);*/ + + /* For debugging purposes, we zero the whole master structure. + * But the application has already set the err pointer, and may have set diff --git a/kpvcrlib/jpeg_decompress_struct_size.patch b/kpvcrlib/jpeg_decompress_struct_size.patch new file mode 100644 index 000000000..deaf375a1 --- /dev/null +++ b/kpvcrlib/jpeg_decompress_struct_size.patch @@ -0,0 +1,15 @@ +--- jdapimin.c 2012-04-04 01:09:00.000000000 +0800 ++++ jdapimin-patched.c 2012-04-04 01:42:44.000000000 +0800 +@@ -36,9 +36,9 @@ + cinfo->mem = NULL; /* so jpeg_destroy knows mem mgr not called */ + if (version != JPEG_LIB_VERSION) + ERREXIT2(cinfo, JERR_BAD_LIB_VERSION, JPEG_LIB_VERSION, version); +- if (structsize != SIZEOF(struct jpeg_decompress_struct)) +- ERREXIT2(cinfo, JERR_BAD_STRUCT_SIZE, +- (int) SIZEOF(struct jpeg_decompress_struct), (int) structsize); ++ /*if (structsize != SIZEOF(struct jpeg_decompress_struct))*/ ++ /*ERREXIT2(cinfo, JERR_BAD_STRUCT_SIZE, */ ++ /*(int) SIZEOF(struct jpeg_decompress_struct), (int) structsize);*/ + + /* For debugging purposes, we zero the whole master structure. + * But the application has already set the err pointer, and may have set diff --git a/launchpad/kpdf.ini b/launchpad/kpdf.ini index 4fd46d72e..4ad186020 100755 --- a/launchpad/kpdf.ini +++ b/launchpad/kpdf.ini @@ -1,3 +1,7 @@ [Actions] -P P = !/mnt/us/launchpad/kpdf.sh +# start kindlepdfviewer with filebrowser in /mnt/us/documents P D = !/mnt/us/launchpad/kpdf.sh /mnt/us/documents +# start kindlepdfviewer with last document +P P = !/mnt/us/launchpad/kpdf.sh +# restart amazon framework - when it got irritated +P R = !/etc/init.d/framework restart diff --git a/launchpad/kpdf.sh b/launchpad/kpdf.sh index 6d1eba1a4..5c0611f2e 100755 --- a/launchpad/kpdf.sh +++ b/launchpad/kpdf.sh @@ -3,6 +3,14 @@ export LC_ALL="en_US.UTF-8" echo unlock > /proc/keypad echo unlock > /proc/fiveway + cd /mnt/us/kindlepdfviewer/ -./reader.lua $1 + +grep /mnt/us/kindlepdfviewer/fonts/host /proc/mounts || mount -o bind /usr/java/lib/fonts /mnt/us/kindlepdfviewer/fonts/host + +./reader.lua "$1" 2> /mnt/us/kindlepdfviewer/crash.log || cat /mnt/us/kindlepdfviewer/crash.log + +grep /mnt/us/kindlepdfviewer/fonts/host /proc/mounts && umount /mnt/us/kindlepdfviewer/fonts/host + +killall -cont cvm echo 1 > /proc/eink_fb/update_display diff --git a/mupdf.patch b/mupdf.patch new file mode 100644 index 000000000..1eec8aa70 --- /dev/null +++ b/mupdf.patch @@ -0,0 +1,171 @@ +diff --git a/pdf/pdf_font.c b/pdf/pdf_font.c +index 5e54e0b..38bd1d8 100644 +--- a/pdf/pdf_font.c ++++ b/pdf/pdf_font.c +@@ -182,8 +182,13 @@ pdf_load_builtin_font(fz_context *ctx, pdf_font_desc *fontdesc, char *fontname) + if (!data) + fz_throw(ctx, "cannot find builtin font: '%s'", fontname); + ++#ifndef NOBUILTINFONT + fontdesc->font = fz_new_font_from_memory(ctx, data, len, 0, 1); + /* RJW: "cannot load freetype font from memory" */ ++#else ++ fontdesc->font = fz_new_font_from_file(ctx, data, 0, 1); ++ free(data); ++#endif + + if (!strcmp(fontname, "Symbol") || !strcmp(fontname, "ZapfDingbats")) + fontdesc->flags |= PDF_FD_SYMBOLIC; +@@ -199,8 +204,13 @@ pdf_load_substitute_font(fz_context *ctx, pdf_font_desc *fontdesc, int mono, int + if (!data) + fz_throw(ctx, "cannot find substitute font"); + ++#ifndef NOBUILTINFONT + fontdesc->font = fz_new_font_from_memory(ctx, data, len, 0, 1); + /* RJW: "cannot load freetype font from memory" */ ++#else ++ fontdesc->font = fz_new_font_from_file(ctx, data, 0, 1); ++ free(data); ++#endif + + fontdesc->font->ft_substitute = 1; + fontdesc->font->ft_bold = bold && !ft_is_bold(fontdesc->font->ft_face); +@@ -218,7 +228,12 @@ pdf_load_substitute_cjk_font(fz_context *ctx, pdf_font_desc *fontdesc, int ros, + fz_throw(ctx, "cannot find builtin CJK font"); + + /* a glyph bbox cache is too big for droid sans fallback (51k glyphs!) */ ++#ifndef NOBUILTINFONT + fontdesc->font = fz_new_font_from_memory(ctx, data, len, 0, 0); ++#else ++ fontdesc->font = fz_new_font_from_file(ctx, data, 0, 0); ++ free(data); ++#endif + /* RJW: "cannot load builtin CJK font" */ + + fontdesc->font->ft_substitute = 1; +diff --git a/pdf/pdf_fontfile.c b/pdf/pdf_fontfile.c +index 543ce76..a076033 100644 +--- a/pdf/pdf_fontfile.c ++++ b/pdf/pdf_fontfile.c +@@ -1,6 +1,8 @@ + #include "fitz.h" + #include "mupdf.h" + ++#ifndef NOBUILTINFONT ++ + #ifdef NOCJK + #define NOCJKFONT + #endif +@@ -129,3 +131,112 @@ pdf_find_substitute_cjk_font(int ros, int serif, unsigned int *len) + return NULL; + #endif + } ++ ++#else // NOBUILTINFONT ++ ++unsigned char * ++get_font_file(char *name) ++{ ++ char *fontdir; ++ char *filename; ++ int len; ++ fontdir = getenv("FONTDIR"); ++ if(fontdir == NULL) { ++ fontdir = "./fonts"; ++ } ++ len = strlen(fontdir) + strlen(name) + 2; ++ filename = malloc(len); ++ if(filename == NULL) { ++ return NULL; ++ } ++ snprintf(filename, len, "%s/%s", fontdir, name); ++ return filename; ++} ++ ++unsigned char * ++pdf_find_builtin_font(char *name, unsigned int *len) ++{ ++ *len = 0; ++ if (!strcmp("Courier", name)) { ++ return get_font_file("NimbusMonL-Regu.cff"); ++ } ++ if (!strcmp("Courier-Bold", name)) { ++ return get_font_file("NimbusMonL-Bold.cff"); ++ } ++ if (!strcmp("Courier-Oblique", name)) { ++ return get_font_file("NimbusMonL-ReguObli.cff"); ++ } ++ if (!strcmp("Courier-BoldOblique", name)) { ++ return get_font_file("NimbusMonL-BoldObli.cff"); ++ } ++ if (!strcmp("Helvetica", name)) { ++ return get_font_file("NimbusSanL-Regu.cff"); ++ } ++ if (!strcmp("Helvetica-Bold", name)) { ++ return get_font_file("NimbusSanL-Bold.cff"); ++ } ++ if (!strcmp("Helvetica-Oblique", name)) { ++ return get_font_file("NimbusSanL-ReguItal.cff"); ++ } ++ if (!strcmp("Helvetica-BoldOblique", name)) { ++ return get_font_file("NimbusSanL-BoldItal.cff"); ++ } ++ if (!strcmp("Times-Roman", name)) { ++ return get_font_file("NimbusRomNo9L-Regu.cff"); ++ } ++ if (!strcmp("Times-Bold", name)) { ++ return get_font_file("NimbusRomNo9L-Medi.cff"); ++ } ++ if (!strcmp("Times-Italic", name)) { ++ return get_font_file("NimbusRomNo9L-ReguItal.cff"); ++ } ++ if (!strcmp("Times-BoldItalic", name)) { ++ return get_font_file("NimbusRomNo9L-MediItal.cff"); ++ } ++ if (!strcmp("Symbol", name)) { ++ return get_font_file("StandardSymL.cff"); ++ } ++ if (!strcmp("ZapfDingbats", name)) { ++ return get_font_file("Dingbats.cff"); ++ } ++ return NULL; ++} ++ ++unsigned char * ++pdf_find_substitute_font(int mono, int serif, int bold, int italic, unsigned int *len) ++{ ++ if (mono) { ++ if (bold) { ++ if (italic) return pdf_find_builtin_font("Courier-BoldOblique", len); ++ else return pdf_find_builtin_font("Courier-Bold", len); ++ } else { ++ if (italic) return pdf_find_builtin_font("Courier-Oblique", len); ++ else return pdf_find_builtin_font("Courier", len); ++ } ++ } else if (serif) { ++ if (bold) { ++ if (italic) return pdf_find_builtin_font("Times-BoldItalic", len); ++ else return pdf_find_builtin_font("Times-Bold", len); ++ } else { ++ if (italic) return pdf_find_builtin_font("Times-Italic", len); ++ else return pdf_find_builtin_font("Times-Roman", len); ++ } ++ } else { ++ if (bold) { ++ if (italic) return pdf_find_builtin_font("Helvetica-BoldOblique", len); ++ else return pdf_find_builtin_font("Helvetica-Bold", len); ++ } else { ++ if (italic) return pdf_find_builtin_font("Helvetica-Oblique", len); ++ else return pdf_find_builtin_font("Helvetica", len); ++ } ++ } ++} ++ ++unsigned char * ++pdf_find_substitute_cjk_font(int ros, int serif, unsigned int *len) ++{ ++ *len = 0; ++ return get_font_file("droid/DroidSansFallback.ttf"); ++} ++ ++#endif // NOBUILTINFONT diff --git a/mupdfimg.c b/mupdfimg.c new file mode 100644 index 000000000..cdabaafca --- /dev/null +++ b/mupdfimg.c @@ -0,0 +1,154 @@ +/* + KindlePDFViewer: MuPDF abstraction for Lua, only image part + Copyright (C) 2012 Hans-Werner Hilse + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#include + +#include "blitbuffer.h" +#include "mupdfimg.h" +#include +#include + + +typedef struct Image { + fz_pixmap *pixmap; + fz_context *context; +} Image; + +static int newImage(lua_State *L) { + int cache_size = luaL_optint(L, 1, 8 << 20); // 8 MB limit default + + Image *img = (Image*) lua_newuserdata(L, sizeof(Image)); + + img->pixmap = NULL; + + luaL_getmetatable(L, "image"); + lua_setmetatable(L, -2); + + img->context = fz_new_context(NULL, NULL, cache_size); + + return 1; +} + +static int loadPNGData(lua_State *L) { + Image *img = (Image*) luaL_checkudata(L, 1, "image"); + size_t length; + unsigned char *data = luaL_checklstring(L, 2, &length); + fz_try(img->context) { + img->pixmap = fz_load_png(img->context, data, length); + } + fz_catch(img->context) { + return luaL_error(L, "cannot load PNG data"); + } +} + +static int loadJPEGData(lua_State *L) { + Image *img = (Image*) luaL_checkudata(L, 1, "image"); + size_t length; + unsigned char *data = luaL_checklstring(L, 2, &length); + fz_try(img->context) { + img->pixmap = fz_load_jpeg(img->context, data, length); + } + fz_catch(img->context) { + return luaL_error(L, "cannot open JPEG data"); + } +} + +static int toBlitBuffer(lua_State *L) { + Image *img = (Image*) luaL_checkudata(L, 1, "image"); + BlitBuffer *bb; + int ret; + int w, h; + + fz_pixmap *pix; + + if(img->pixmap == NULL) { + return luaL_error(L, "no pixmap loaded that we could convert"); + } + + if(img->pixmap->n == 2) { + pix = img->pixmap; + } else { + fz_try(img->context) { + pix = fz_new_pixmap(img->context, fz_device_gray, img->pixmap->w, img->pixmap->h); + } + fz_catch(img->context) { + return luaL_error(L, "can't claim new grayscale fz_pixmap"); + } + fz_convert_pixmap(img->context, img->pixmap, pix); + } + + ret = newBlitBufferNative(L, img->pixmap->w, img->pixmap->h, &bb); + if(ret != 1) { + // TODO (?): fail more gracefully, clean up mem? + return ret; + } + + uint8_t *bbptr = (uint8_t*)bb->data; + uint16_t *pmptr = (uint16_t*)pix->samples; + int x, y; + + for(y = 0; y < bb->h; y++) { + for(x = 0; x < (bb->w / 2); x++) { + bbptr[x] = (((pmptr[x*2 + 1] & 0xF0) >> 4) | (pmptr[x*2] & 0xF0)) ^ 0xFF; + } + if(bb->w & 1) { + bbptr[x] = (pmptr[x*2] & 0xF0) ^ 0xF0; + } + bbptr += bb->pitch; + pmptr += bb->w; + } + + if(pix != img->pixmap) { + fz_drop_pixmap(img->context, pix); + } + + return 1; +} + +static int freeImage(lua_State *L) { + Image *img = (Image*) luaL_checkudata(L, 1, "image"); + if(img->pixmap) { + fz_drop_pixmap(img->context, img->pixmap); + } + fz_free_context(img->context); + return 0; +} + +static const struct luaL_Reg mupdfimg_func[] = { + {"new", newImage}, + {NULL, NULL} +}; + +static const struct luaL_Reg image_meth[] = { + {"loadPNGData", loadPNGData}, + {"loadJPEGData", loadJPEGData}, + {"toBlitBuffer", toBlitBuffer}, + {"free", freeImage}, + {"__gc", freeImage}, + {NULL, NULL} +}; + +int luaopen_mupdfimg(lua_State *L) { + luaL_newmetatable(L, "image"); + lua_pushstring(L, "__index"); + lua_pushvalue(L, -2); + lua_settable(L, -3); + luaL_register(L, NULL, image_meth); + lua_pop(L, 1); + luaL_register(L, "mupdfimg", mupdfimg_func); + return 1; +} diff --git a/mupdfimg.h b/mupdfimg.h new file mode 100644 index 000000000..48b6d4f94 --- /dev/null +++ b/mupdfimg.h @@ -0,0 +1,26 @@ +/* + KindlePDFViewer: MuPDF abstraction for Lua, only image part + Copyright (C) 2012 Hans-Werner Hilse + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#ifndef _MUPDFIMG_H +#define _MUPDFIMG_H + +#include +#include +#include + +int luaopen_mupdfimg(lua_State *L); +#endif diff --git a/pdf.c b/pdf.c index 6c904030f..b47ea8926 100644 --- a/pdf.c +++ b/pdf.c @@ -164,16 +164,16 @@ fz_alloc_context my_alloc_default = static int openDocument(lua_State *L) { char *filename = strdup(luaL_checkstring(L, 1)); - int cachesize = luaL_optint(L, 2, 64 << 20); // 64 MB limit default + int cache_size = luaL_optint(L, 2, 64 << 20); // 64 MB limit default char buf[15]; - printf("cachesize: %s\n",readable_fs(cachesize,buf)); + printf("## cache_size: %s\n",readable_fs(cache_size,buf)); PdfDocument *doc = (PdfDocument*) lua_newuserdata(L, sizeof(PdfDocument)); luaL_getmetatable(L, "pdfdocument"); lua_setmetatable(L, -2); - doc->context = fz_new_context(&my_alloc_default, NULL, cachesize); + doc->context = fz_new_context(&my_alloc_default, NULL, cache_size); fz_try(doc->context) { doc->xref = fz_open_document(doc->context, filename); @@ -311,6 +311,125 @@ static int openPage(lua_State *L) { return 1; } +/* get the text of the given page + * + * will return text in a Lua table that is modeled after + * djvu.c creates this table. + * + * note that the definition of "line" is somewhat arbitrary + * here (for now) + * + * MuPDFs API provides text as single char information + * that is collected in "spans". we use a span as a "line" + * in Lua output and segment spans into words by looking + * for space characters. + * + * will return an empty table if we have no text + */ +static int getPageText(lua_State *L) { + fz_text_span *page_text; + fz_text_span *ptr; + fz_device *tdev; + fz_bbox bbox, linebbox; + int i; + int word, line; + int len, c; + int start; + char chars[4]; // max length of UTF-8 encoded rune + luaL_Buffer textbuf; + + PdfPage *page = (PdfPage*) luaL_checkudata(L, 1, "pdfpage"); + + page_text = fz_new_text_span(page->doc->context); + tdev = fz_new_text_device(page->doc->context, page_text); + fz_run_page(page->doc->xref, page->page, tdev, fz_identity, NULL); + fz_free_device(tdev); + + /* table that contains all the lines */ + lua_newtable(L); + line = 1; + for(ptr = page_text; ptr != NULL; ptr = ptr->next) { + if(ptr->text == NULL) continue; + + /* table for the words */ + lua_newtable(L); + word = 1; + linebbox = ptr->text[0].bbox; // start with sensible default + for(i = 0; i < ptr->len; ) { + /* will hold information about a word: */ + lua_newtable(L); + + luaL_buffinit(L, &textbuf); + bbox = ptr->text[i].bbox; // start with sensible default + for(; i < ptr->len; i++) { + /* check for space characters */ + if(ptr->text[i].c == ' ' || + ptr->text[i].c == '\t' || + ptr->text[i].c == '\n' || + ptr->text[i].c == '\v' || + ptr->text[i].c == '\f' || + ptr->text[i].c == '\r' || + ptr->text[i].c == 0xA0 || + ptr->text[i].c == 0x1680 || + ptr->text[i].c == 0x180E || + (ptr->text[i].c >= 0x2000 && ptr->text[i].c <= 0x200A) || + ptr->text[i].c == 0x202F || + ptr->text[i].c == 0x205F || + ptr->text[i].c == 0x3000) { + // ignore and end word + i++; + break; + } + len = runetochar(chars, &ptr->text[i].c); + for(c = 0; c < len; c++) { + luaL_addchar(&textbuf, chars[c]); + } + bbox = fz_union_bbox(bbox, ptr->text[i].bbox); + linebbox = fz_union_bbox(linebbox, ptr->text[i].bbox); + } + lua_pushstring(L, "word"); + luaL_pushresult(&textbuf); + lua_settable(L, -3); + + /* bbox for a word: */ + lua_pushstring(L, "x0"); + lua_pushinteger(L, bbox.x0); + lua_settable(L, -3); + lua_pushstring(L, "y0"); + lua_pushinteger(L, bbox.y0); + lua_settable(L, -3); + lua_pushstring(L, "x1"); + lua_pushinteger(L, bbox.x1); + lua_settable(L, -3); + lua_pushstring(L, "y1"); + lua_pushinteger(L, bbox.y1); + lua_settable(L, -3); + + lua_rawseti(L, -2, word++); + } + + /* bbox for a whole line (or in fact, a "span") */ + lua_pushstring(L, "x0"); + lua_pushinteger(L, linebbox.x0); + lua_settable(L, -3); + lua_pushstring(L, "y0"); + lua_pushinteger(L, linebbox.y0); + lua_settable(L, -3); + lua_pushstring(L, "x1"); + lua_pushinteger(L, linebbox.x1); + lua_settable(L, -3); + lua_pushstring(L, "y1"); + lua_pushinteger(L, linebbox.y1); + lua_settable(L, -3); + + lua_rawseti(L, -2, line++); + } + + fz_free_text_span(page->doc->context, page_text); + + return 1; +} + static int getPageSize(lua_State *L) { fz_matrix ctm; fz_rect bounds; @@ -323,7 +442,7 @@ static int getPageSize(lua_State *L) { ctm = fz_concat(ctm, fz_rotate(dc->rotate)); bbox = fz_transform_rect(ctm, bounds); - lua_pushnumber(L, bbox.x1-bbox.x0); + lua_pushnumber(L, bbox.x1-bbox.x0); lua_pushnumber(L, bbox.y1-bbox.y0); return 2; @@ -423,6 +542,17 @@ static int drawPage(lua_State *L) { return 0; } +static int getCacheSize(lua_State *L) { + printf("## mupdf getCacheSize = %d\n", msize); + lua_pushnumber(L, msize); + return 1; +} + +static int cleanCache(lua_State *L) { + printf("## mupdf cleanCache NOP\n"); + return 0; +} + static const struct luaL_Reg pdf_func[] = { {"openDocument", openDocument}, {NULL, NULL} @@ -433,8 +563,10 @@ static const struct luaL_Reg pdfdocument_meth[] = { {"authenticatePassword", authenticatePassword}, {"openPage", openPage}, {"getPages", getNumberOfPages}, - {"getTOC", getTableOfContent}, + {"getToc", getTableOfContent}, {"close", closeDocument}, + {"getCacheSize", getCacheSize}, + {"cleanCache", cleanCache}, {"__gc", closeDocument}, {NULL, NULL} }; @@ -442,6 +574,7 @@ static const struct luaL_Reg pdfdocument_meth[] = { static const struct luaL_Reg pdfpage_meth[] = { {"getSize", getPageSize}, {"getUsedBBox", getUsedBBox}, + {"getPageText", getPageText}, {"close", closePage}, {"__gc", closePage}, {"draw", drawPage}, diff --git a/pdfreader.lua b/pdfreader.lua index ec4e1a105..4c2772104 100644 --- a/pdfreader.lua +++ b/pdfreader.lua @@ -8,12 +8,12 @@ function PDFReader:open(filename) -- muPDF manages its own cache, set second parameter -- to the maximum size you want it to grow local ok - ok, self.doc = pcall(pdf.openDocument, filename, 64*1024*1024) + ok, self.doc = pcall(pdf.openDocument, filename, self.cache_document_size) if not ok then return false, self.doc -- will contain error message end if self.doc:needsPassword() then - local password = InputBox:input(height-100, 100, "Pass:") + local password = InputBox:input(G_height-100, 100, "Pass:") if not password or not self.doc:authenticatePassword(password) then self.doc:close() self.doc = nil @@ -30,3 +30,18 @@ function PDFReader:open(filename) end return true end + +---------------------------------------------------- +-- highlight support +---------------------------------------------------- +function PDFReader:getText(pageno) + local ok, page = pcall(self.doc.openPage, self.doc, pageno) + if not ok then + -- TODO: error handling + return nil + end + local text = page:getPageText() + --print("## page:getPageText "..dump(text)) -- performance impact on device + page:close() + return text +end diff --git a/reader.lua b/reader.lua index 0ad9da911..f1aed3f34 100755 --- a/reader.lua +++ b/reader.lua @@ -20,11 +20,13 @@ require "alt_getopt" require "pdfreader" require "djvureader" +require "crereader" require "filechooser" require "settings" require "screen" require "keys" require "commands" +require "dialog" -- option parsing: longopts = { @@ -42,17 +44,23 @@ function openFile(filename) reader = DJVUReader elseif file_type == "pdf" or file_type == "xps" or file_type == "cbz" then reader = PDFReader + elseif file_type == "epub" or file_type == "txt" or file_type == "rtf" or file_type == "htm" or file_type == "html" or file_type == "fb2" or file_type == "chm" then + reader = CREReader end if reader then + InfoMessage:show("Opening document, please wait... ") + fb:refresh(0) local ok, err = reader:open(filename) if ok then reader:loadSettings(filename) - page_num = reader.settings:readSetting("last_page") or 1 + page_num = reader:getLastPageOrPos() reader:goto(tonumber(page_num)) reader_settings:savesetting("lastfile", filename) return reader:inputLoop() else - -- TODO: error handling + InfoMessage:show("Error opening document.") + fb:refresh(0) + util.sleep(2) end end return true -- on failed attempts, we signal to keep running @@ -66,9 +74,6 @@ function showusage() print("-g, --goto=page start reading on page") print("-G, --gamma=GAMMA set gamma correction") print(" (floating point notation, e.g. \"1.5\")") - print("-d, --device=DEVICE set device specific configuration,") - print(" currently one of \"kdxg\" (default), \"k3\"") - print(" \"emu\" (DXG emulation)") print("-h, --help show this usage help") print("") print("If you give the name of a directory instead of a file path, a file") @@ -86,18 +91,12 @@ if optarg["h"] then return showusage() end - -if optarg["d"] == "k3" then - -- for now, the only difference is the additional input device - input.open("/dev/input/event0") - input.open("/dev/input/event1") - input.open("/dev/input/event2") - setK3Keycodes() -elseif optarg["d"] == "emu" then +if util.isEmulated()==1 then input.open("") -- SDL key codes setEmuKeycodes() else + input.open("slider") input.open("/dev/input/event0") input.open("/dev/input/event1") @@ -116,16 +115,16 @@ if optarg["G"] ~= nil then end fb = einkfb.open("/dev/fb0") -width, height = fb:getSize() +G_width, G_height = fb:getSize() -- read current rotation mode Screen:updateRotationMode() -origin_rotation_mode = Screen.cur_rotation_mode +Screen.native_rotation_mode = Screen.cur_rotation_mode -- set up reader's setting: font reader_settings = DocSettings:open(".reader") -r_cfont = reader_settings:readSetting("cfont") -if r_cfont ~=nil then - Font.cfont = r_cfont +fontmap = reader_settings:readSetting("fontmap") +if fontmap ~= nil then + Font.fontmap = fontmap end -- initialize global settings shared among all readers @@ -133,6 +132,7 @@ UniReader:initGlobalSettings(reader_settings) -- initialize specific readers PDFReader:init() DJVUReader:init() +CREReader:init() -- display directory or open file local patharg = reader_settings:readSetting("lastfile") @@ -140,12 +140,13 @@ if ARGV[optind] and lfs.attributes(ARGV[optind], "mode") == "directory" then local running = true FileChooser:setPath(ARGV[optind]) while running do - local file, callback = FileChooser:choose(0,height) + local file, callback = FileChooser:choose(0, G_height) if callback then callback() else if file ~= nil then running = openFile(file) + print(file) else running = false end @@ -161,15 +162,15 @@ end -- save reader settings -reader_settings:savesetting("cfont", Font.cfont) +reader_settings:savesetting("fontmap", Font.fontmap) reader_settings:close() -- @TODO dirty workaround, find a way to force native system poll -- screen orientation and upside down mode 09.03 2012 -fb:setOrientation(origin_rotation_mode) +fb:setOrientation(Screen.native_rotation_mode) input.closeAll() ---os.execute('test -e /proc/keypad && echo "send '..KEY_HOME..'" > /proc/keypad ') -if optarg["d"] ~= "emu" then +if util.isEmulated()==0 then + --os.execute("killall -cont cvm") os.execute('echo "send '..KEY_MENU..'" > /proc/keypad;echo "send '..KEY_MENU..'" > /proc/keypad') end diff --git a/rendertext.lua b/rendertext.lua index 337d86285..32dd2e084 100644 --- a/rendertext.lua +++ b/rendertext.lua @@ -13,6 +13,7 @@ function glyphCacheClaim(size) glyphcache[k].age = glyphcache[k].age - 1 else glyphcache_current_memsize = glyphcache_current_memsize - glyphcache[k].size + glyphcache[k].glyph.bb:free() glyphcache[k] = nil end end @@ -20,10 +21,10 @@ function glyphCacheClaim(size) glyphcache_current_memsize = glyphcache_current_memsize + size return true end -function getGlyph(face, facehash, charcode) - local hash = glyphCacheHash(facehash, charcode) +function getGlyph(face, charcode) + local hash = glyphCacheHash(face.hash, charcode) if glyphcache[hash] == nil then - local glyph = face:renderGlyph(charcode) + local glyph = face.ftface:renderGlyph(charcode) local size = glyph.bb:getWidth() * glyph.bb:getHeight() / 2 + 32 glyphCacheClaim(size); glyphcache[hash] = { @@ -88,9 +89,9 @@ function renderUtf8Text(buffer, x, y, face, facehash, text, kerning, backgroundC for uchar in string.gfind(text, "([%z\1-\127\194-\244][\128-\191]*)") do if pen_x < buffer:getWidth() then local charcode = util.utf8charcode(uchar) - local glyph = getGlyph(face, facehash, charcode) + local glyph = getGlyph(face, charcode) if kerning and prevcharcode then - local kern = face:getKerning(prevcharcode, charcode) + local kern = face.ftface:getKerning(prevcharcode, charcode) pen_x = pen_x + kern --print("prev:"..string.char(prevcharcode).." curr:"..string.char(charcode).." pen_x:"..pen_x.." kern:"..kern) buffer:addblitFrom(glyph.bb, x + pen_x + glyph.l, y - glyph.t, 0, 0, glyph.bb:getWidth(), glyph.bb:getHeight()) diff --git a/rendertext_example.lua b/rendertext_example.lua deleted file mode 100755 index 428dda716..000000000 --- a/rendertext_example.lua +++ /dev/null @@ -1,36 +0,0 @@ -#!./kpdfview -require "rendertext" -require "graphics" - -fb = einkfb.open("/dev/fb0") -width, height = fb:getSize() - -print("open") -size = 50 ---face = freetype.newBuiltinFace("sans", 64) -face = freetype.newFace("/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf", size) -print("got face") - -if face:hasKerning() then - print("has kerning") -end - -width, height = fb:getSize() -fb.bb:paintRect(5,5,width-5,height-5,4); - -faceHeight, faceAscender = face:getHeightAndAscender(); -print("face height:"..tostring(faceHeight).." - ascender:"..faceAscender) -faceHeight = math.ceil(faceHeight) -faceAscender = math.ceil(faceAscender) -print("face height:"..tostring(faceHeight).." - ascender:"..faceAscender) - -posY = 5 + faceAscender -renderUtf8Text(fb.bb, 5, posY, face, "h", "AV T.T: gxyt!", true) -posY = posY + faceHeight -renderUtf8Text(fb.bb, 5, posY, face, "h2", "AV T.T: gxyt!", false) - -fb:refresh() - -while true do - local ev = input.waitForEvent() -end diff --git a/resources/info-i.png b/resources/info-i.png new file mode 100644 index 000000000..bf68406f4 Binary files /dev/null and b/resources/info-i.png differ diff --git a/screen.lua b/screen.lua index 24923a2e7..f7c9b3f6c 100644 --- a/screen.lua +++ b/screen.lua @@ -41,6 +41,11 @@ Codes for rotation modes: Screen = { cur_rotation_mode = 0, + -- these two variabls are used to help switching from framework to reader + native_rotation_mode = nil, + kpv_rotation_mode = nil, + + saved_bb = nil, } -- @orien: 1 for clockwise rotate, -1 for anti-clockwise @@ -71,4 +76,29 @@ function Screen:updateRotationMode() end end +function Screen:saveCurrentBB() + local width, height = G_width, G_height + if not self.saved_bb then + self.saved_bb = Blitbuffer.new(width, height) + end + if self.saved_bb:getWidth() ~= width then + self.saved_bb:free() + self.saved_bb = Blitbuffer.new(width, height) + end + self.saved_bb:blitFullFrom(fb.bb) +end + +function Screen:restoreFromSavedBB() + self:restoreFromBB(self.saved_bb) +end + +function Screen:getCurrentScreenBB() + local bb = Blitbuffer.new(G_width, G_height) + bb:blitFullFrom(fb.bb) + return bb +end + +function Screen:restoreFromBB(bb) + fb.bb:blitFullFrom(bb) +end diff --git a/selectmenu.lua b/selectmenu.lua index 3572a5c99..da6f609bd 100644 --- a/selectmenu.lua +++ b/selectmenu.lua @@ -2,6 +2,7 @@ require "rendertext" require "keys" require "graphics" require "font" +require "commands" SelectMenu = { -- font for displaying item names @@ -11,8 +12,7 @@ SelectMenu = { -- font for paging display ffsize = 16, -- font for item shortcut - sface = freetype.newBuiltinFace("mono", 22), - sfhash = "mono22", + sface = Font:getFace("scfont", 22), -- title height title_H = 40, @@ -21,7 +21,7 @@ SelectMenu = { -- foot height foot_H = 27, - menu_title = "None Titled", + menu_title = "No Title", no_item_msg = "No items found.", item_array = {}, items = 0, @@ -32,10 +32,14 @@ SelectMenu = { "Z", "X", "C", "V", "B", "N", "M", ".", "Sym", "Ent", }, last_shortcut = 0, + -- state buffer page = 1, current = 1, oldcurrent = 0, + selected_item = nil, + + commands = nil, } function SelectMenu:new(o) @@ -46,10 +50,12 @@ function SelectMenu:new(o) o.page = 1 o.current = 1 o.oldcurrent = 0 + o.selected_item = nil -- increase spacing for DXG so we don't have more than 30 shortcuts if fb.bb:getHeight() == 1200 then o.spacing = 37 end + o:addAllCommands() return o end @@ -62,74 +68,197 @@ function SelectMenu:getItemIndexByShortCut(c, perpage) end end ---[ +function SelectMenu:addAllCommands() + self.commands = Commands:new{} + + self.commands:add(KEY_FW_UP, nil, "", + "previous item", + function(sm) + if sm.current == 1 then + if sm.page > 1 then + sm.current = sm.perpage + sm.page = sm.page - 1 + sm.pagedirty = true + end + else + sm.current = sm.current - 1 + sm.markerdirty = true + end + end + ) + self.commands:add(KEY_FW_DOWN, nil, "", + "next item", + function(sm) + if sm.current == sm.perpage then + if sm.page < (sm.items / sm.perpage) then + sm.current = 1 + sm.page = sm.page + 1 + sm.pagedirty = true + end + else + if sm.page ~= math.floor(sm.items / sm.perpage) + 1 + or sm.current + (sm.page - 1) * sm.perpage < sm.items then + sm.current = sm.current + 1 + sm.markerdirty = true + end + end + end + ) + self.commands:add({KEY_PGFWD, KEY_LPGFWD}, nil, "", + "next page", + function(sm) + if sm.page < (sm.items / sm.perpage) then + if sm.current + sm.page * sm.perpage > sm.items then + sm.current = sm.items - sm.page * sm.perpage + end + sm.page = sm.page + 1 + sm.pagedirty = true + else + sm.current = sm.items - (sm.page - 1) * sm.perpage + sm.markerdirty = true + end + end + ) + self.commands:add({KEY_PGBCK, KEY_LPGBCK}, nil, "", + "previous page", + function(sm) + if sm.page > 1 then + sm.page = sm.page - 1 + sm.pagedirty = true + else + sm.current = 1 + sm.markerdirty = true + end + end + ) + self.commands:add(KEY_FW_PRESS, nil, "", + "select menu item", + function(sm) + if sm.last_shortcut < 30 then + if sm.items == 0 then + return "break" + else + self.selected_item = (sm.perpage * (sm.page - 1) + + sm.current) + end + end + end + ) + local KEY_Q_to_P = {} + for i = KEY_Q, KEY_P do + table.insert(KEY_Q_to_P, Keydef:new(i, nil, "")) + end + self.commands:addGroup("Q to P", KEY_Q_to_P, + "Select menu item with Q to E key as shortcut", + function(sm, keydef) + sm.selected_item = sm:getItemIndexByShortCut( + sm.item_shortcuts[ keydef.keycode - KEY_Q + 1 ], sm.perpage) + end + ) + local KEY_A_to_L = {} + for i = KEY_A, KEY_L do + table.insert(KEY_A_to_L, Keydef:new(i, nil, "")) + end + self.commands:addGroup("A to L", KEY_A_to_L, + "Select menu item with A to L key as shortcut", + function(sm, keydef) + sm.selected_item = sm:getItemIndexByShortCut( + sm.item_shortcuts[ keydef.keycode - KEY_A + 11 ], sm.perpage) + end + ) + local KEY_Z_to_M = {} + for i = KEY_Z, KEY_M do + table.insert(KEY_Z_to_M, Keydef:new(i, nil, "")) + end + self.commands:addGroup("Z to M", KEY_Z_to_M, + "Select menu item with Z to M key as shortcut", + function(sm, keydef) + sm.selected_item = sm:getItemIndexByShortCut( + sm.item_shortcuts[ keydef.keycode - KEY_Z + 21 ], sm.perpage) + end + ) + self.commands:add(KEY_DEL, nil, "", + "Select menu item with del key as shortcut", + function(sm) + sm.selected_item = sm:getItemIndexByShortCut("Del", sm.perpage) + end + ) + self.commands:add(KEY_DOT, nil, "", + "Select menu item with dot key as shortcut", + function(sm) + sm.selected_item = sm:getItemIndexByShortCut(".", sm.perpage) + end + ) + self.commands:add({KEY_SYM, KEY_SLASH}, nil, "", + "Select menu item with sym/slash key as shortcut", + function(sm) + -- DXG has slash after dot + sm.selected_item = sm:getItemIndexByShortCut("Sym", sm.perpage) + end + ) + self.commands:add(KEY_ENTER, nil, "", + "Select menu item with enter key as shortcut", + function(sm) + sm.selected_item = sm:getItemIndexByShortCut("Ent", sm.perpage) + end + ) + self.commands:add(KEY_BACK, nil, "", + "Exit menu", + function(sm) + return "break" + end + ) +end + +function SelectMenu:clearCommands() + self.commands = Commands:new{} + + self.commands:add(KEY_BACK, nil, "", + "Exit menu", + function(sm) + return "break" + end) +end + +------------------------------------------------ -- return the index of selected item ---] +------------------------------------------------ function SelectMenu:choose(ypos, height) - local perpage = math.floor(height / self.spacing) - 2 - local pagedirty = true - local markerdirty = false - - local prevItem = function () - if self.current == 1 then - if self.page > 1 then - self.current = perpage - self.page = self.page - 1 - pagedirty = true - end - else - self.current = self.current - 1 - markerdirty = true - end - end - - local nextItem = function () - if self.current == perpage then - if self.page < (self.items / perpage) then - self.current = 1 - self.page = self.page + 1 - pagedirty = true - end - else - if self.page ~= math.floor(self.items / perpage) + 1 - or self.current + (self.page-1)*perpage < self.items then - self.current = self.current + 1 - markerdirty = true - end - end - end - + self.perpage = math.floor(height / self.spacing) - 2 + self.pagedirty = true + self.markerdirty = false self.last_shortcut = 0 while true do - local cface, cfhash = Font:getFaceAndHash(22) - local tface, tfhash = Font:getFaceAndHash(25, Font.tfont) - local fface, ffhash = Font:getFaceAndHash(16, Font.ffont) + local cface = Font:getFace("cfont", 22) + local tface = Font:getFace("tfont", 25) + local fface = Font:getFace("ffont", 16) - if pagedirty then - markerdirty = true + if self.pagedirty then + self.markerdirty = true -- draw menu title fb.bb:paintRect(0, ypos, fb.bb:getWidth(), self.title_H + 10, 0) fb.bb:paintRect(10, ypos + 10, fb.bb:getWidth() - 20, self.title_H, 5) local x = 20 local y = ypos + self.title_H - renderUtf8Text(fb.bb, x, y, tface, tfhash, self.menu_title, true) + renderUtf8Text(fb.bb, x, y, tface, self.menu_title, true) -- draw items fb.bb:paintRect(0, ypos + self.title_H + 10, fb.bb:getWidth(), height - self.title_H, 0) if self.items == 0 then y = ypos + self.title_H + (self.spacing * 2) - renderUtf8Text(fb.bb, 30, y, cface, cfhash, + renderUtf8Text(fb.bb, 30, y, cface, "Oops... Bad news for you:", true) y = y + self.spacing - renderUtf8Text(fb.bb, 30, y, cface, cfhash, + renderUtf8Text(fb.bb, 30, y, cface, self.no_item_msg, true) - markerdirty = false + self.markerdirty = false + self:clearCommands() else local c - for c = 1, perpage do - local i = (self.page - 1) * perpage + c + for c = 1, self.perpage do + local i = (self.page - 1) * self.perpage + c if i <= self.items then y = ypos + self.title_H + (self.spacing * c) @@ -142,30 +271,32 @@ function SelectMenu:choose(ypos, height) if self.item_shortcuts[c] ~= nil and string.len(self.item_shortcuts[c]) == 3 then -- print "Del", "Sym and "Ent" - renderUtf8Text(fb.bb, 13, y, fface, ffhash, + renderUtf8Text(fb.bb, 13, y, fface, self.item_shortcuts[c], true) else - renderUtf8Text(fb.bb, 18, y, self.sface, self.sfhash, + renderUtf8Text(fb.bb, 18, y, self.sface, self.item_shortcuts[c], true) end self.last_shortcut = c - renderUtf8Text(fb.bb, 50, y, cface, cfhash, + renderUtf8Text(fb.bb, 50, y, cface, self.item_array[i], true) - end - end - end + end -- EOF if i <= self.items + end -- EOF for + end -- EOF if -- draw footer - y = ypos + self.title_H + (self.spacing * perpage) + self.foot_H + 5 + y = ypos + self.title_H + (self.spacing * self.perpage) + + self.foot_H + 5 x = (fb.bb:getWidth() / 2) - 50 - renderUtf8Text(fb.bb, x, y, fface, ffhash, - "Page "..self.page.." of "..(math.floor(self.items / perpage)+1), true) + renderUtf8Text(fb.bb, x, y, fface, + "Page "..self.page.." of ".. + (math.ceil(self.items / self.perpage)), true) end - if markerdirty then - if not pagedirty then + if self.markerdirty then + if not self.pagedirty then if self.oldcurrent > 0 then y = ypos + self.title_H + (self.spacing * self.oldcurrent) + 8 fb.bb:paintRect(45, y, fb.bb:getWidth() - 60, 3, 0) @@ -175,72 +306,41 @@ function SelectMenu:choose(ypos, height) -- draw new marker line y = ypos + self.title_H + (self.spacing * self.current) + 8 fb.bb:paintRect(45, y, fb.bb:getWidth() - 60, 3, 15) - if not pagedirty then + if not self.pagedirty then fb:refresh(1, 45, y, fb.bb:getWidth() - 60, 3) end self.oldcurrent = self.current - markerdirty = false + self.markerdirty = false end - if pagedirty then + if self.pagedirty then fb:refresh(0, 0, ypos, fb.bb:getWidth(), height) - pagedirty = false + self.pagedirty = false end - local ev = input.waitForEvent() + local ev = input.saveWaitForEvent() ev.code = adjustKeyEvents(ev) if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then - local selected = nil - if ev.code == KEY_FW_UP then - prevItem() - elseif ev.code == KEY_FW_DOWN then - nextItem() - 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 - end - self.page = self.page + 1 - pagedirty = true - else - self.current = self.items - (self.page-1)*perpage - markerdirty = true - end - elseif ev.code == KEY_PGBCK or ev.code == KEY_LPGBCK then - if self.page > 1 then - self.page = self.page - 1 - pagedirty = true - else - self.current = 1 - markerdirty = true - end - elseif ev.code == KEY_FW_PRESS or ev.code == KEY_ENTER and self.last_shortcut < 30 then - if self.items == 0 then - return nil - else - return (perpage*(self.page-1) + self.current) - end - elseif ev.code >= KEY_Q and ev.code <= KEY_P then - selected = self:getItemIndexByShortCut(self.item_shortcuts[ ev.code - KEY_Q + 1 ], perpage) - elseif ev.code >= KEY_A and ev.code <= KEY_L then - selected = self:getItemIndexByShortCut(self.item_shortcuts[ ev.code - KEY_A + 11], perpage) - elseif ev.code >= KEY_Z and ev.code <= KEY_M then - selected = self:getItemIndexByShortCut(self.item_shortcuts[ ev.code - KEY_Z + 21], perpage) - elseif ev.code == KEY_DEL then - selected = self:getItemIndexByShortCut("Del", perpage) - elseif ev.code == KEY_DOT then - selected = self:getItemIndexByShortCut(".", perpage) - elseif ev.code == KEY_SYM or ev.code == KEY_SLASH then -- DXG has slash after dot - selected = self:getItemIndexByShortCut("Sym", perpage) - elseif ev.code == KEY_ENTER then - selected = self:getItemIndexByShortCut("Ent", perpage) - elseif ev.code == KEY_BACK then - return nil + keydef = Keydef:new(ev.code, getKeyModifier()) + print("key pressed: "..tostring(keydef)) + + command = self.commands:getByKeydef(keydef) + if command ~= nil then + print("command to execute: "..tostring(command)) + ret_code = command.func(self, keydef) + else + print("command not found: "..tostring(command)) end - if selected ~= nil then - print("# selected "..selected) - return selected + + if ret_code == "break" then + break end - end - end + + if self.selected_item ~= nil then + print("# selected "..self.selected_item) + return self.selected_item, self.item_array[self.selected_item] + end + end -- EOF if + end -- EOF while + return nil end diff --git a/slider_watcher.c b/slider_watcher.c new file mode 100644 index 000000000..6a4e26e15 --- /dev/null +++ b/slider_watcher.c @@ -0,0 +1,87 @@ +/* + KindlePDFViewer: power slider key event watcher + Copyright (C) 2012 Qingping Hou + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define OUTPUT_SIZE 21 +#define EVENT_PIPE "/tmp/event_slider" +#define CODE_IN_SAVER 10000 +#define CODE_OUT_SAVER 10001 + +int +main ( int argc, char *argv[] ) +{ + int fd, ret; + FILE *fp; + char std_out[OUTPUT_SIZE] = ""; + struct input_event ev; + __u16 key_code = 10000; + + /* create the npipe if not exists */ + /*if(access(argv[1], F_OK)){*/ + /*printf("npipe %s not found, try to create it...\n", argv[1]);*/ + /*if(mkfifo(argv[1], 0777)) {*/ + /*printf("Create npipe %s failed!\n", argv[1]);*/ + /*}*/ + /*}*/ + + /* open npipe for writing */ + fd = open(argv[1], O_RDWR | O_NONBLOCK); + if(fd < 0) { + printf("Open %s falied: %s\n", argv[1], strerror(errno)); + exit(EXIT_FAILURE); + } + + /* initialize event struct */ + ev.type = EV_KEY; + ev.code = key_code; + ev.value = 1; + + while(1) { + /* listen power slider events */ + memset(std_out, 0, OUTPUT_SIZE); + fp = popen("lipc-wait-event -s 0 com.lab126.powerd goingToScreenSaver,outOfScreenSaver", "r"); + ret = fread(std_out, OUTPUT_SIZE, 1, fp); + pclose(fp); + + /* fill event struct */ + gettimeofday(&ev.time, NULL); + if(std_out[0] == 'g') { + ev.code = CODE_IN_SAVER; + } else if(std_out[0] == 'o') { + ev.code = CODE_OUT_SAVER; + } else { + printf("Unrecognized event.\n"); + exit(EXIT_FAILURE); + } + + /* generate event */ + ret = write(fd, &ev, sizeof(struct input_event)); + } + + close(fd); + return EXIT_SUCCESS; +} 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/test/tall.pdf b/test/tall.pdf new file mode 100644 index 000000000..c58ebc3d8 Binary files /dev/null and b/test/tall.pdf differ diff --git a/unireader.lua b/unireader.lua index bb067a723..4cf59ba84 100644 --- a/unireader.lua +++ b/unireader.lua @@ -34,11 +34,16 @@ 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, + dest_x = 0, -- real offset_x when it's smaller than screen, so it's centered + dest_y = 0, min_offset_x = 0, min_offset_y = 0, content_top = 0, -- for ZOOM_FIT_TO_CONTENT_WIDTH_PAN (prevView) @@ -51,6 +56,7 @@ UniReader = { pan_y = 0, pan_margin = 20, -- horizontal margin for two-column zoom pan_overlap_vertical = 30, + show_overlap = 0, -- the document: doc = nil, @@ -64,15 +70,17 @@ UniReader = { -- tile cache configuration: cache_max_memsize = 1024*1024*5, -- 5MB tile cache - cache_item_max_pixels = 1024*1024*2, -- max. size of rendered tiles cache_max_ttl = 20, -- time to live -- tile cache state: cache_current_memsize = 0, cache = {}, + -- renderer cache size + cache_document_size = 1024*1024*8, -- FIXME random, needs testing pagehash = nil, jump_stack = {}, + highlight = {}, toc = nil, bbox = {}, -- override getUsedBBox @@ -85,58 +93,796 @@ function UniReader:new(o) return o end ---[[ - For a new specific reader, - you must always overwrite following two methods: +---------------------------------------------------- +-- !!!!!!!!!!!!!!!!!!!!!!!!! +-- +-- For a new specific reader, +-- you must always overwrite following method: +-- +-- * self:open() +-- +-- overwrite other methods if needed. +---------------------------------------------------- - * self:open() - - overwrite other methods if needed. ---]] -function UniReader:init() +-- open a file +function UniReader:open(filename, cache_size) + return false end --- open a file and its settings store --- tips: you can use self:loadSettings in open() method. -function UniReader:open(filename, password) - return false +function UniReader:init() + -- initialize commands + self:addAllCommands() +end + +---------------------------------------------------- +-- highlight support +---------------------------------------------------- + +function UniReader:screenOffset() + local x = self.dest_x + local y = self.dest_y + if self.offset_x < 0 then + x = x + self.offset_x + end + if self.offset_y < 0 then + y = y + self.offset_y + end + print("# screenOffset "..x..","..y) + return x,y +end + +---------------------------------------------------- +-- Given coordinates of four corners in original page +-- size and return coordinate of upper left conner in +-- zoomed page size with width and height. +---------------------------------------------------- +function UniReader:zoomedRectCoordTransform(x0, y0, x1, y1) + local x,y = self:screenOffset() + return + x0 * self.globalzoom + x, + y0 * self.globalzoom + y, + (x1 - x0) * self.globalzoom, + (y1 - y0) * self.globalzoom +end + +---------------------------------------------------- +-- Given coordinates of four corners in original page +-- size and return rectangular area in screen. You +-- might want to call this when you want to draw stuff +-- on screen. +-- +-- NOTE: this method does not check whether given area +-- is can be shown in current screen. Make sure to check +-- with _isEntireWordInScreenRange() or _isWordInScreenRange() +-- before you want to draw on the returned area. +---------------------------------------------------- +function UniReader:getRectInScreen(x0, y0, x1, y1) + x, y, w, h = self:zoomedRectCoordTransform(x0, y0, x1, y1) + if x < 0 then + w = w + x + x = 0 + end + if y < 0 then + h = h + y + y = 0 + end + if x + w > G_width then w = G_width - x end + if y + h > G_height then h = G_height - y end + return x, y, w, h +end + +-- make sure the whole word/line can be seen in screen +-- @TODO when not in FIT_TO_CONTENT_WIDTH mode, +-- self.offset_{x,y} might be negative. 12.04 2012 (houqp) +function UniReader:_isEntireLineInScreenHeightRange(l) + return (l ~= nil) and + (l.y0 * self.globalzoom) >= -self.offset_y + and (l.y1 * self.globalzoom) <= -self.offset_y + G_height +end + +function UniReader:_isEntireWordInScreenRange(w) + return self:_isEntireWordInScreenHeightRange(w) and + self:_isEntireWordInScreenWidthRange(w) +end + +function UniReader:_isEntireWordInScreenHeightRange(w) + return (w ~= nil) and + (w.y0 * self.globalzoom) >= -self.offset_y + and (w.y1 * self.globalzoom) <= -self.offset_y + G_height +end + +function UniReader:_isEntireWordInScreenWidthRange(w) + return (w ~= nil) and + (w.x0 * self.globalzoom >= -self.offset_x) and + (w.x1 * self.globalzoom <= -self.offset_x + G_width) +end + +-- make sure at least part of the word can be seen in screen +function UniReader:_isWordInScreenRange(w) + if not w then + return false + end + + is_entire_word_out_of_screen_height = + (w.y1 * self.globalzoom <= -self.offset_y) + or (w.y0 * self.globalzoom >= -self.offset_y + G_height) + + is_entire_word_out_of_screen_width = + (w.x0 * self.globalzoom >= -self.offset_x + G_width + or w.x1 * self.globalzoom <= -self.offset_x) + + return (not is_entire_word_out_of_screen_height) and + (not is_entire_word_out_of_screen_width) +end + +function UniReader: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:_isWordInScreenRange(line_item) then + local x, y, w, h = self:getRectInScreen( + 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 -- if isEntireWordInScreenHeightRange + end -- for line_item + end -- for text_item +end + +function UniReader:_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 -- closure +end + +function UniReader:_toggleWordHighLight(t, l, w) + x, y, w, h = self:getRectInScreen(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 UniReader:_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 UniReader:drawCursorAfterWord(t, l, w) + -- get height of line t[l][w] is in + local _, _, _, h = self:zoomedRectCoordTransform(0, t[l].y0, 0, t[l].y1) + -- get rect of t[l][w] + local x, y, wd, _ = self:getRectInScreen(t[l][w].x0, t[l][w].y0, t[l][w].x1, t[l][w].y1) + self.cursor:setHeight(h) + self.cursor:moveTo(x+wd, y) + self.cursor:draw() +end + +function UniReader:drawCursorBeforeWord(t, l, w) + -- get height of line t[l][w] is in + local _, _, _, h = self:zoomedRectCoordTransform(0, t[l].y0, 0, t[l].y1) + -- get rect of t[l][w] + local x, y, _, _ = self:getRectInScreen(t[l][w].x0, t[l][w].y0, t[l][w].x1, t[l][w].y1) + self.cursor:setHeight(h) + self.cursor:moveTo(x, y) + self.cursor:draw() +end + +function UniReader:getText(pageno) + -- define a sensible implementation when your reader supports it + return nil +end + +function UniReader:startHighLightMode() + local t = self:getText(self.pageno) + if not t or #t == 0 then + return nil + end + + local function _findFirstWordInView(t) + for i=1, #t, 1 do + if self:_isEntireWordInScreenRange(t[i][1]) then + return i, 1 + end + end + + print("## _findFirstWordInView none found in "..dump(t)) + + return nil + end + + local function _isMovingForward(l, w) + return l.cur > l.start or (l.cur == l.start and w.cur > w.start) + end + + --------------------------------------- + -- some word handling help functions + --------------------------------------- + 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 + + --------------------------------------- + -- some gap handling help functions + --------------------------------------- + local function _nextGap(t, cur_l, cur_w) + local is_meet_end = false + + -- handle left end of line as special case. + if cur_w == 0 then + if cur_l == #t and #t[cur_l] == 1 then + is_meet_end = true + end + return cur_l, 1, is_meet_end + end + + cur_l, cur_w = _nextWord(t, cur_l, cur_w) + if cur_w == 1 then + cur_w = 0 + end + if cur_w ~= 0 and cur_l == #t and cur_w == #t[cur_l] then + is_meet_end = true + end + return cur_l, cur_w, is_meet_end + end + + local function _prevGap(t, cur_l, cur_w) + local is_meet_start = false + + -- handle left end of line as special case. + if cur_l == 1 and (cur_w == 1 or cur_w == 0) then -- in the first line + is_meet_start = true + return cur_l, 0, is_meet_start + end + if cur_w == 1 then -- not in the first line + return cur_l, 0, is_meet_start + elseif cur_w == 0 then + -- set to 1 so _prevWord() can find previous word in previous line + cur_w = 1 + end + + cur_l, cur_w = _prevWord(t, cur_l, cur_w) + return cur_l, cur_w, is_meet_end + end + + local function _gapInNextLine(t, cur_l, cur_w) + local is_meet_end = false + + if cur_l == #t then + -- already in last line + cur_w = #t[cur_l] + is_meet_end = true + else + -- handle left end of line as special case. + if cur_w == 0 then + cur_l = math.min(cur_l + 1, #t) + else + cur_l, cur_w = _wordInNextLine(t, cur_l, cur_w) + end + end + + return cur_l, cur_w, is_meet_end + end + + local function _gapInPrevLine(t, cur_l, cur_w) + local is_meet_start = false + + if cur_l == 1 then + -- already in first line + is_meet_start = true + cur_w = 0 + else + if cur_w == 0 then + -- goto left end of previous line + cur_l = math.max(cur_l - 1, 1) + else + cur_l, cur_w = _wordInPrevLine(t, cur_l, cur_w) + end + end + + return cur_l, cur_w, is_meet_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 + + local cx, cy, cw, ch = self:getRectInScreen( + t[l.cur][w.cur].x0, + t[l.cur][w.cur].y0, + t[l.cur][w.cur].x1, + t[l.cur][w.cur].y1) + + self.cursor = Cursor:new { + x_pos = cx+cw, + y_pos = cy, + h = ch, + 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.saveWaitForEvent() + ev.code = adjustKeyEvents(ev) + if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then + if ev.code == KEY_FW_LEFT and not is_meet_start then + is_meet_end = false + l.new, w.new, is_meet_start = _prevGap(t, l.cur, w.cur) + + self.cursor:clear() + if w.new ~= 0 + and not self:_isEntireLineInScreenHeightRange(t[l.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.new == 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_RIGHT and not is_meet_end then + is_meet_start = false + l.new, w.new, is_meet_end = _nextGap(t, l.cur, w.cur) + + self.cursor:clear() + -- we want to check whether the word is in screen range, + -- so trun gap into word + local tmp_w = w.new + if tmp_w == 0 then + tmp_w = 1 + end + if not self:_isEntireLineInScreenHeightRange(t[l.new]) + and self:_isEntireWordInScreenWidthRange(t[l.new][tmp_w]) then + local pageno = self:nextView() + self:goto(pageno) + end + + if w.new == 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 and not is_meet_start then + is_meet_end = false + l.new, w.new, is_meet_start = _gapInPrevLine(t, l.cur, w.cur) + + self.cursor:clear() + + local tmp_w = w.new + if tmp_w == 0 then + tmp_w = 1 + end + if not self:_isEntireLineInScreenHeightRange(t[l.new]) + 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 and not is_meet_end then + is_meet_start = false + l.new, w.new, is_meet_end = _gapInNextLine(t, l.cur, w.cur) + + self.cursor:clear() + + local tmp_w = w.new + if w.cur == 0 then + tmp_w = 1 + end + if not self:_isEntireLineInScreenHeightRange(t[l.new]) + 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 -- for line_item + end -- for text_item + end -- 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 + 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 -- if check key event + l.cur, w.cur = l.new, w.new + fb:refresh(1) + end + end -- while running + --print("start", l.cur, w.cur, l.start, w.start) + + -- two helper functions for highlight + local function _togglePrevWordHighLight(t, l, w) + if w.cur == 0 then + if l.cur == 1 then + -- already at the begin of first line, nothing to toggle + return l, w, true + else + w.cur = 1 + end + end + 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:_isEntireLineInScreenHeightRange(t[l.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:_isEntireLineInScreenHeightRange(t[l.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.saveWaitForEvent() + 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 -- if 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 -- not is_meet_start + elseif ev.code == KEY_FW_DOWN then + is_meet_start = false + if not is_meet_end then + -- handle left end of first line as special case + if w.cur == 0 then + tmp_w = 1 + else + tmp_w = w.cur + end + tmp_l, tmp_w = _wordInNextLine(t, l.cur, tmp_w) + 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 -- if key event + fb:refresh(1) + end + end -- EOF while +end + + +---------------------------------------------------- +-- Renderer memory +---------------------------------------------------- + +function UniReader:getCacheSize() + return -1 +end + +function UniReader:cleanCache() + return +end + +---------------------------------------------------- +-- Setting related methods +---------------------------------------------------- + +-- load special settings for specific reader +function UniReader:loadSpecialSettings() + return +end + +-- save special settings for specific reader +function UniReader:saveSpecialSettings() end --[ following are default methods ]-- -function UniReader:loadSettings(filename) - if self.doc ~= nil then - self.settings = DocSettings:open(filename) - - local gamma = self.settings:readSetting("gamma") - if gamma then - self.globalgamma = gamma - end - - local jumpstack = self.settings:readSetting("jumpstack") - self.jump_stack = jumpstack or {} - - local bbox = self.settings:readSetting("bbox") - print("# bbox loaded "..dump(bbox)) - self.bbox = bbox - - self.globalzoom = self.settings:readSetting("globalzoom") or 1.0 - self.globalzoommode = self.settings:readSetting("globalzoommode") or -1 - - return true - end - return false -end - function UniReader:initGlobalSettings(settings) + local pan_margin = settings:readSetting("pan_margin") + if pan_margin then + self.pan_margin = pan_margin + end + local pan_overlap_vertical = settings:readSetting("pan_overlap_vertical") if pan_overlap_vertical then self.pan_overlap_vertical = pan_overlap_vertical end - -- initialize commands - self:addAllCommands() local cache_max_memsize = settings:readSetting("cache_max_memsize") if cache_max_memsize then @@ -147,6 +893,48 @@ function UniReader:initGlobalSettings(settings) if cache_max_ttl then self.cache_max_ttl = cache_max_ttl end + + local rcountmax = settings:readSetting("partial_refresh_count") + if rcountmax then + self.rcountmax = rcountmax + end +end + +-- This is a low-level method that can be shared with all readers. +function UniReader:loadSettings(filename) + if self.doc ~= nil then + self.settings = DocSettings:open(filename,self.cache_document_size) + + local gamma = self.settings:readSetting("gamma") + if gamma then + self.globalgamma = gamma + end + + 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 + + self.globalzoom = self.settings:readSetting("globalzoom") or 1.0 + self.globalzoommode = self.settings:readSetting("globalzoommode") or -1 + + self:loadSpecialSettings() + return true + end + return false +end + +function UniReader:getLastPageOrPos() + return self.settings:readSetting("last_page") or 1 +end + +function UniReader:saveLastPageOrPos() + self.settings:savesetting("last_page", self.pageno) end -- guarantee that we have enough memory in cache @@ -165,6 +953,7 @@ function UniReader:cacheClaim(size) else -- cache slot is at end of life, so kick it out self.cache_current_memsize = self.cache_current_memsize - self.cache[k].size + self.cache[k].bb:free() self.cache[k] = nil end end @@ -182,11 +971,12 @@ function UniReader:drawOrCache(no, preCache) -- ideally, this should be factored out and only be called when needed (TODO) local ok, page = pcall(self.doc.openPage, self.doc, no) + local width, height = G_width, G_height if not ok then -- 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. @@ -237,7 +1027,7 @@ function UniReader:drawOrCache(no, preCache) tile.y = 0 tile.w = self.fullwidth tile.h = self.fullheight - elseif (tile.w*tile.h / 2) > max_cache then + elseif (tile.w*tile.h / 2) < max_cache then -- no, we can't. so generate a tile as big as we can go -- grow area in steps of 10px while ((tile.w+10) * (tile.h+10) / 2) < max_cache do @@ -252,7 +1042,7 @@ function UniReader:drawOrCache(no, preCache) tile.y = tile.y - 5 tile.h = tile.h + 5 end - if tile.y + tile.h < self.fullheigth then + if tile.y + tile.h < self.fullheight then tile.h = tile.h + 5 end end @@ -286,14 +1076,18 @@ end -- blank the cache function UniReader:clearCache() + for k, _ in pairs(self.cache) do + self.cache[k].bb:free() + end self.cache = {} self.cache_current_memsize = 0 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) + local width, height = G_width, G_height print("# page::getSize "..pwidth.."*"..pheight); local x0, y0, x1, y1 = page:getUsedBBox() if x0 == 0.01 and y0 == 0.01 and x1 == -0.01 and y1 == -0.01 then @@ -302,6 +1096,8 @@ function UniReader:setZoom(page) x1 = pwidth y1 = pheight end + if x1 == 0 then x1 = pwidth end + if y1 == 0 then y1 = pheight end -- clamp to page BBox if x0 < 0 then x0 = 0 end if x1 > pwidth then x1 = pwidth end @@ -386,7 +1182,7 @@ function UniReader:setZoom(page) if self.content_top == -2012 then -- We must handle previous page turn as a special cases, -- because we want to arrive at the bottom of previous page. - -- Since this a real page turn, we need to recalcunate stuff. + -- Since this a real page turn, we need to recalculate stuff. if (x1 - x0) < pwidth then self.globalzoom = width / (x1 - x0) end @@ -404,11 +1200,13 @@ function UniReader:setZoom(page) or self.globalzoommode == self.ZOOM_FIT_TO_CONTENT_HALF_WIDTH_MARGIN then local margin = self.pan_margin if self.globalzoommode == self.ZOOM_FIT_TO_CONTENT_HALF_WIDTH then margin = 0 end - self.globalzoom = width / (x1 - x0 + margin) + local pg_margin = 0 -- margin scaled to page size + if margin > 0 then pg_margin = margin * 2 / self.globalzoom end + self.globalzoom = width / (x1 - x0 + pg_margin) self.offset_x = -1 * x0 * self.globalzoom * 2 + margin - self.globalzoom = height / (y1 - y0) + self.globalzoom = height / (y1 - y0 + pg_margin) self.offset_y = -1 * y0 * self.globalzoom * 2 + margin - self.globalzoom = width / (x1 - x0 + margin) * 2 + self.globalzoom = width / (x1 - x0 + pg_margin) * 2 print("column mode offset:"..self.offset_x.."*"..self.offset_y.." zoom:"..self.globalzoom); self.globalzoommode = self.ZOOM_BY_VALUE -- enable pan mode self.pan_x = self.offset_x @@ -421,6 +1219,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 @@ -443,37 +1245,53 @@ end -- render and blit a page function UniReader:show(no) local pagehash, offset_x, offset_y = self:drawOrCache(no) + local width, height = G_width, G_height + if not pagehash then return end self.pagehash = pagehash local bb = self.cache[pagehash].bb - local dest_x = 0 - local dest_y = 0 + self.dest_x = 0 + self.dest_y = 0 if bb:getWidth() - offset_x < width then -- we can't fill the whole output width, center the content - dest_x = (width - (bb:getWidth() - offset_x)) / 2 + self.dest_x = (width - (bb:getWidth() - offset_x)) / 2 end if bb:getHeight() - offset_y < height and self.globalzoommode ~= self.ZOOM_FIT_TO_CONTENT_WIDTH_PAN then -- we can't fill the whole output height and not in -- ZOOM_FIT_TO_CONTENT_WIDTH_PAN mode, center the content - dest_y = (height - (bb:getHeight() - offset_y)) / 2 + self.dest_y = (height - (bb:getHeight() - offset_y)) / 2 elseif self.globalzoommode == self.ZOOM_FIT_TO_CONTENT_WIDTH_PAN and self.offset_y > 0 then -- if we are in ZOOM_FIT_TO_CONTENT_WIDTH_PAN mode and turning to -- the top of the page, we might leave an empty space between the -- page top and screen top. - dest_y = self.offset_y + self.dest_y = self.offset_y end - if dest_x or dest_y then + if self.dest_x or self.dest_y then fb.bb:paintRect(0, 0, width, height, 8) end - print("# blitFrom dest_off:("..dest_x..", "..dest_y.. + print("# blitFrom dest_off:("..self.dest_x..", "..self.dest_y.. "), src_off:("..offset_x..", "..offset_y.."), ".. "width:"..width..", height:"..height) - fb.bb:blitFrom(bb, dest_x, dest_y, offset_x, offset_y, width, height) - if self.rcount == self.rcountmax then + fb.bb:blitFrom(bb, self.dest_x, self.dest_y, offset_x, offset_y, width, height) + + print("## self.show_overlap "..self.show_overlap) + if self.show_overlap < 0 then + fb.bb:dimRect(0,0, width, self.dest_y - self.show_overlap) + elseif self.show_overlap > 0 then + fb.bb:dimRect(0,self.dest_y + height - self.show_overlap, width, self.show_overlap) + end + self.show_overlap = 0 + + -- 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 fb:refresh(0) @@ -485,27 +1303,32 @@ function UniReader:show(no) self.slot_visible = slot; end +function UniReader:isSamePage(p1, p2) + return p1 == p2 +end + --[[ @ pageno is the page you want to add to jump_stack + NOTE: for CREReader, pageno refers to xpointer --]] function UniReader:addJump(pageno, notes) local jump_item = nil local notes_to_add = notes if not notes_to_add then - -- no notes given, auto generate from TOC entry - notes_to_add = self:getTOCTitleByPage(self.pageno) + -- no notes given, auto generate from Toc entry + notes_to_add = self:getTocTitleOfCurrentPage() if notes_to_add ~= "" then notes_to_add = "in "..notes_to_add end end -- move pageno page to jump_stack top if already in for _t,_v in ipairs(self.jump_stack) do - if _v.page == pageno then + if self:isSamePage(_v.page, pageno) then jump_item = _v table.remove(self.jump_stack, _t) -- if original notes is not empty, probably defined by users, -- we use the original notes to overwrite auto generated notes - -- from TOC entry + -- from Toc entry if jump_item.notes ~= "" then notes_to_add = jump_item.notes end @@ -563,6 +1386,10 @@ function UniReader:goto(no) end end +function UniReader:redrawCurrentPage() + self:goto(self.pageno) +end + function UniReader:nextView() local pageno = self.pageno @@ -573,7 +1400,9 @@ function UniReader:nextView() pageno = pageno + 1 else -- goto next view of current page - self.offset_y = self.offset_y - height + self.pan_overlap_vertical + self.offset_y = self.offset_y - G_height + + self.pan_overlap_vertical + self.show_overlap = -self.pan_overlap_vertical -- top < 0 end else -- not in fit to content width pan mode, just do a page turn @@ -599,7 +1428,9 @@ function UniReader:prevView() pageno = pageno - 1 else -- goto previous view of current page - self.offset_y = self.offset_y + height - self.pan_overlap_vertical + self.offset_y = self.offset_y + G_height + - self.pan_overlap_vertical + self.show_overlap = self.pan_overlap_vertical -- bottom > 0 end else -- not in fit to content width pan mode, just do a page turn @@ -618,14 +1449,14 @@ end function UniReader:modifyGamma(factor) print("modifyGamma, gamma="..self.globalgamma.." factor="..factor) self.globalgamma = self.globalgamma * factor; - self:goto(self.pageno) + self:redrawCurrentPage() end -- adjust zoom state and trigger re-rendering function UniReader:setGlobalZoomMode(newzoommode) if self.globalzoommode ~= newzoommode then self.globalzoommode = newzoommode - self:goto(self.pageno) + self:redrawCurrentPage() end end @@ -634,35 +1465,35 @@ function UniReader:setGlobalZoom(zoom) if self.globalzoom ~= zoom then self.globalzoommode = self.ZOOM_BY_VALUE self.globalzoom = zoom - self:goto(self.pageno) + self:redrawCurrentPage() end end function UniReader:setRotate(rotate) self.globalrotate = rotate - self:goto(self.pageno) + self:redrawCurrentPage() end -- @ orien: 1 for clockwise rotate, -1 for anti-clockwise function UniReader:screenRotate(orien) Screen:screenRotate(orien) - width, height = fb:getSize() + -- update global width and height variable + G_width, G_height = fb:getSize() self:clearCache() - self:goto(self.pageno) end -function UniReader:cleanUpTOCTitle(title) +function UniReader:cleanUpTocTitle(title) return title:gsub("\13", "") end -function UniReader:fillTOC() - self.toc = self.doc:getTOC() +function UniReader:fillToc() + self.toc = self.doc:getToc() end -function UniReader:getTOCTitleByPage(pageno) +function UniReader:getTocTitleByPage(pageno) if not self.toc then -- build toc when needed. - self:fillTOC() + self:fillToc() end -- no table of content @@ -677,81 +1508,145 @@ function UniReader:getTOCTitleByPage(pageno) end pre_entry = _v end - return self:cleanUpTOCTitle(pre_entry.title) + return self:cleanUpTocTitle(pre_entry.title) end -function UniReader:showTOC() +function UniReader:getTocTitleOfCurrentPage() + return self:getTocTitleByPage(self.pageno) +end + +function UniReader:gotoTocEntry(entry) + self:goto(entry.page) +end + +function UniReader:showToc() if not self.toc then - -- build toc when needed. - self:fillTOC() + -- build toc if needed. + self:fillToc() end - local menu_items = {} - local filtered_toc = {} + -- build menu items - for _k,_v in ipairs(self.toc) do + local menu_items = {} + 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)) end - toc_menu = SelectMenu:new{ - menu_title = "Table of Contents", - item_array = menu_items, - no_item_msg = "This document does not have a Table of Contents.", - } - item_no = toc_menu:choose(0, fb.bb:getHeight()) - if item_no then - self:goto(filtered_toc[item_no]) + + if #menu_items == 0 then + showInfoMsgWithDelay( + "This document does not have a TOC.", 2000, 1) else - self:goto(self.pageno) + toc_menu = SelectMenu:new{ + menu_title = "Table of Contents", + item_array = menu_items, + } + item_no = toc_menu:choose(0, fb.bb:getHeight()) + + if item_no then + self:gotoTocEntry(self.toc[item_no]) + else + self:redrawCurrentPage() + end end 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..")", - item_array = menu_items, - no_item_msg = "No jump history.", - } - item_no = jump_menu:choose(0, fb.bb:getHeight()) - if item_no then - local jump_item = self.jump_stack[item_no] - self:goto(jump_item.page) + + if #menu_items == 0 then + showInfoMsgWithDelay( + "No jump history found.", 2000, 1) else - self:goto(self.pageno) + jump_menu = SelectMenu:new{ + menu_title = "Jump Keeper (current page: "..self.pageno..")", + item_array = menu_items, + } + item_no = jump_menu:choose(0, fb.bb:getHeight()) + if item_no then + local jump_item = self.jump_stack[item_no] + self:goto(jump_item.page) + else + self:redrawCurrentPage() + end end end -function UniReader:showMenu() - local ypos = height - 50 +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 + if #menu_items == 0 then + showInfoMsgWithDelay( + "No HighLights found.", 2000, 1) + else + toc_menu = SelectMenu:new{ + menu_title = "HighLights", + item_array = menu_items, + } + item_no = toc_menu:choose(0, fb.bb:getHeight()) + if item_no then + self:goto(highlight_dict[item_no].page) + else + self:redrawCurrentPage() + end + end +end + +-- used in UniReader:showMenu() +function UniReader:_drawReadingInfo() + local width, height = G_width, G_height local load_percent = (self.pageno / self.doc:getPages()) + local face = Font:getFace("cfont", 22) + -- display memory on top of page + fb.bb:paintRect(0, 0, width, 15+6*2, 0) + renderUtf8Text(fb.bb, 10, 15+6, face, + "Memory: ".. + math.ceil( self.cache_current_memsize / 1024 ).."/"..math.ceil( self.cache_max_memsize / 1024 ).. + " "..math.ceil( self.doc:getCacheSize() / 1024 ).."/"..math.ceil( self.cache_document_size / 1024 ).." k", + true) + + -- display reading progress on bottom of page + local ypos = height - 50 fb.bb:paintRect(0, ypos, width, 50, 0) - ypos = ypos + 15 - local face, fhash = Font:getFaceAndHash(22) - local cur_section = self:getTOCTitleByPage(self.pageno) + local cur_section = self:getTocTitleOfCurrentPage() if cur_section ~= "" then cur_section = "Section: "..cur_section end - renderUtf8Text(fb.bb, 10, ypos+6, face, fhash, + renderUtf8Text(fb.bb, 10, ypos+6, face, "Page: "..self.pageno.."/"..self.doc:getPages().. " "..cur_section, true) ypos = ypos + 15 blitbuffer.progressBar(fb.bb, 10, ypos, width-20, 15, 5, 4, load_percent, 8) +end + +function UniReader:showMenu() + self:_drawReadingInfo() + fb:refresh(1) while 1 do - local ev = input.waitForEvent() + local ev = input.saveWaitForEvent() ev.code = adjustKeyEvents(ev) if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then if ev.code == KEY_BACK or ev.code == KEY_MENU then return + elseif ev.code == KEY_C then + self.doc:cleanCache() end end end @@ -770,7 +1665,7 @@ end function UniReader:inputLoop() local keep_running = true while 1 do - local ev = input.waitForEvent() + local ev = input.saveWaitForEvent() ev.code = adjustKeyEvents(ev) if ev.type == EV_KEY and ev.value == EVENT_VALUE_KEY_PRESS then local secs, usecs = util.gettime() @@ -800,12 +1695,14 @@ function UniReader:inputLoop() self.doc:close() end if self.settings ~= nil then - self.settings:savesetting("last_page", self.pageno) + self:saveLastPageOrPos() self.settings:savesetting("gamma", self.globalgamma) self.settings:savesetting("jumpstack", self.jump_stack) 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:saveSpecialSettings() self.settings:close() end @@ -815,7 +1712,6 @@ end -- command definitions function UniReader:addAllCommands() self.commands = Commands:new() - self.commands:addGroup("< >",{Keydef:new(KEY_PGBCK,nil),Keydef:new(KEY_PGFWD,nil)}, "previous/next page", function(unireader,keydef) @@ -907,7 +1803,7 @@ function UniReader:addAllCommands() self.commands:add(KEY_G,nil,"G", "open 'go to page' input box", function(unireader) - local page = InputBox:input(height-100, 100, "Page:") + local page = NumInputBox:input(G_height-100, 100, "Page:") -- convert string to number if not pcall(function () page = page + 0 end) then page = unireader.pageno @@ -921,13 +1817,13 @@ function UniReader:addAllCommands() self.commands:add(KEY_H,nil,"H", "show help page", function(unireader) - HelpPage:show(0,height,unireader.commands) - unireader:goto(unireader.pageno) + HelpPage:show(0, G_height, unireader.commands) + unireader:redrawCurrentPage() end) self.commands:add(KEY_T,nil,"T", "show table of content", function(unireader) - unireader:showTOC() + unireader:showToc() end) self.commands:add(KEY_B,nil,"B", "show jump stack", @@ -939,25 +1835,46 @@ function UniReader:addAllCommands() function(unireader) unireader:addJump(unireader.pageno) end) - self.commands:add(KEY_J,nil,"J", + self.commands:add(KEY_J,MOD_SHIFT,"J", "rotate 10° clockwise", function(unireader) unireader:setRotate( unireader.globalrotate + 10 ) end) - self.commands:add(KEY_J,MOD_SHIFT,"J", + self.commands:add(KEY_J,nil,"J", "rotate screen 90° clockwise", function(unireader) unireader:screenRotate("clockwise") + if self.globalzoommode == self.ZOOM_FIT_TO_CONTENT_WIDTH_PAN then + self:setGlobalZoomMode(self.ZOOM_FIT_TO_CONTENT_WIDTH) + else + self:redrawCurrentPage() + end end) - self.commands:add(KEY_K,nil,"K", + self.commands:add(KEY_K,MOD_SHIFT,"K", "rotate 10° counterclockwise", function(unireader) unireader:setRotate( unireader.globalrotate - 10 ) end) - self.commands:add(KEY_K,MOD_SHIFT,"K", + self.commands:add(KEY_K,nil,"K", "rotate screen 90° counterclockwise", function(unireader) unireader:screenRotate("anticlockwise") + if self.globalzoommode == self.ZOOM_FIT_TO_CONTENT_WIDTH_PAN then + self:setGlobalZoomMode(self.ZOOM_FIT_TO_CONTENT_WIDTH) + else + self:redrawCurrentPage() + end + end) + self.commands:add(KEY_R, MOD_SHIFT, "R", + "manual full screen refresh", + function(unireader) + -- eink will not refresh if nothing is changeed on the screen + -- so we fake a change here. + fb.bb:invertRect(0, 0, 1, 1) + fb:refresh(1) + fb.bb:invertRect(0, 0, 1, 1) + fb:refresh(0) + unireader.rcount = 1 end) self.commands:add(KEY_Z,nil,"Z", "set crop mode", @@ -965,8 +1882,8 @@ function UniReader:addAllCommands() local bbox = {} bbox["x0"] = - unireader.offset_x / unireader.globalzoom bbox["y0"] = - unireader.offset_y / unireader.globalzoom - bbox["x1"] = bbox["x0"] + width / unireader.globalzoom - bbox["y1"] = bbox["y0"] + height / unireader.globalzoom + bbox["x1"] = bbox["x0"] + G_width / unireader.globalzoom + bbox["y1"] = bbox["y0"] + G_height / unireader.globalzoom bbox.pan_x = unireader.pan_x bbox.pan_y = unireader.pan_y unireader.bbox[unireader.pageno] = bbox @@ -991,7 +1908,7 @@ function UniReader:addAllCommands() "toggle info box", function(unireader) unireader:showMenu() - unireader:goto(unireader.pageno) + unireader:redrawCurrentPage() end) -- panning local panning_keys = {Keydef:new(KEY_FW_LEFT,MOD_ANY),Keydef:new(KEY_FW_RIGHT,MOD_ANY),Keydef:new(KEY_FW_UP,MOD_ANY),Keydef:new(KEY_FW_DOWN,MOD_ANY),Keydef:new(KEY_FW_PRESS,MOD_ANY)} @@ -1011,8 +1928,8 @@ function UniReader:addAllCommands() x = unireader.shift_x / 5 y = unireader.shift_y / 5 elseif unireader.pan_by_page then - x = width; - y = height - unireader.pan_overlap_vertical; -- overlap for lines which didn't fit + x = G_width + y = G_height - unireader.pan_overlap_vertical -- overlap for lines which didn't fit else x = unireader.shift_x y = unireader.shift_y @@ -1031,6 +1948,7 @@ function UniReader:addAllCommands() unireader.offset_y = unireader.min_offset_y -- bottom unireader:goto(unireader.pageno - 1) else + unireader.show_overlap = 0 unireader.offset_y = unireader.min_offset_y end elseif unireader.offset_x > 0 then @@ -1045,6 +1963,7 @@ function UniReader:addAllCommands() unireader.offset_y = unireader.pan_y unireader:goto(unireader.pageno + 1) else + unireader.show_overlap = 0 unireader.offset_y = unireader.pan_y end elseif unireader.offset_x < unireader.min_offset_x then @@ -1053,12 +1972,22 @@ function UniReader:addAllCommands() elseif keydef.keycode == KEY_FW_UP then unireader.offset_y = unireader.offset_y + y if unireader.offset_y > 0 then + if unireader.pan_by_page then + unireader.show_overlap = unireader.offset_y + unireader.pan_overlap_vertical + end unireader.offset_y = 0 + elseif unireader.pan_by_page then + unireader.show_overlap = unireader.pan_overlap_vertical -- bottom end elseif keydef.keycode == KEY_FW_DOWN then unireader.offset_y = unireader.offset_y - y if unireader.offset_y < unireader.min_offset_y then + if unireader.pan_by_page then + unireader.show_overlap = unireader.offset_y + y - unireader.min_offset_y - G_height + end unireader.offset_y = unireader.min_offset_y + elseif unireader.pan_by_page then + unireader.show_overlap = -unireader.pan_overlap_vertical -- top end elseif keydef.keycode == KEY_FW_PRESS then if keydef.modifier==MOD_SHIFT then @@ -1079,11 +2008,26 @@ function UniReader:addAllCommands() end if old_offset_x ~= unireader.offset_x or old_offset_y ~= unireader.offset_y then - unireader:goto(unireader.pageno) + unireader:redrawCurrentPage() end end end) -- end panning - --print defined commands - --for k,v in pairs(self.commands.map) do print(v) end + -- highlight mode + 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 + ) + -- commands.map is very large, impacts startup performance on device + --print("## defined commands "..dump(self.commands.map)) end diff --git a/util.c b/util.c index 571792bf4..061847723 100644 --- a/util.c +++ b/util.c @@ -17,6 +17,7 @@ */ #include +#include #include "util.h" @@ -28,6 +29,19 @@ static int gettime(lua_State *L) { return 2; } +static int util_sleep(lua_State *L) { + unsigned int seconds = luaL_optint(L, 1, 0); + sleep(seconds); + return 0; +} + +static int util_usleep(lua_State *L) { + useconds_t useconds = luaL_optint(L, 1, 0); + usleep(useconds); + return 0; +} + +/* Turn UTF-8 char code to Unicode */ static int utf8charcode(lua_State *L) { size_t len; const char* utf8char = luaL_checklstring(L, 1, &len); @@ -46,9 +60,21 @@ static int utf8charcode(lua_State *L) { return 1; } +static int isEmulated(lua_State *L) { +#ifdef EMULATE_READER + lua_pushinteger(L, 1); +#else + lua_pushinteger(L, 0); +#endif + return 1; +} + static const struct luaL_Reg util_func[] = { {"gettime", gettime}, + {"sleep", util_sleep}, + {"usleep", util_usleep}, {"utf8charcode", utf8charcode}, + {"isEmulated", isEmulated}, {NULL, NULL} }; diff --git a/widget.lua b/widget.lua new file mode 100644 index 000000000..aa8ba1034 --- /dev/null +++ b/widget.lua @@ -0,0 +1,263 @@ +require "rendertext" +require "graphics" +require "image" + +--[[ +This is a (useless) generic Widget interface + +widgets can be queried about their size and can be paint. +that's it for now. Probably we need something more elaborate +later. +]] +Widget = { + dimen = { w = 0, h = 0}, +} + +function Widget:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o +end + +function Widget:getSize() + return self.dimen +end + +function Widget:paintTo(bb, x, y) +end + +function Widget:free() +end + +--[[ +WidgetContainer is a container for another Widget +]] +WidgetContainer = Widget:new() + +function WidgetContainer:free() + for _, widget in ipairs(self) do + widget:free() + end +end + +--[[ +CenterContainer centers its content (1 widget) within its own dimensions +]] +CenterContainer = WidgetContainer:new() + +function CenterContainer:paintTo(bb, x, y) + local contentSize = self[1]:getSize() + if contentSize.w > self.dimen.w or contentSize.h > self.dimen.h then + -- throw error? + return + end + self[1]:paintTo(bb, + x + (self.dimen.w - contentSize.w)/2, + y + (self.dimen.h - contentSize.h)/2) +end + +--[[ +A FrameContainer is some graphics content (1 widget) that is surrounded by a frame +]] +FrameContainer = WidgetContainer:new({ + background = nil, + color = 15, + margin = 0, + bordersize = 2, + padding = 5, +}) + +function FrameContainer:getSize() + local content_size = self[1]:getSize() + return { + w = content_size.w + ( self.margin + self.bordersize + self.padding ) * 2, + h = content_size.h + ( self.margin + self.bordersize + self.padding ) * 2 + } +end + +function FrameContainer:paintTo(bb, x, y) + local my_size = self:getSize() + + if self.background then + bb:paintRect(x, y, my_size.w, my_size.h, self.background) + end + if self.bordersize > 0 then + bb:paintBorder(x + self.margin, y + self.margin, + my_size.w - self.margin * 2, my_size.h - self.margin * 2, + self.bordersize, self.color) + end + self[1]:paintTo(bb, + x + self.margin + self.bordersize + self.padding, + y + self.margin + self.bordersize + self.padding) +end + +--[[ +A TextWidget puts a string on a single line +]] +TextWidget = Widget:new({ + text = nil, + face = nil, + color = 15, + _bb = nil, + _length = 0, + _maxlength = 1200, +}) + +function TextWidget:_render() + local h = self.face.size * 1.5 + self._bb = Blitbuffer.new(self._maxlength, h) + self._length = renderUtf8Text(self._bb, 0, h*.7, self.face, self.text, self.color) +end + +function TextWidget:getSize() + if not self._bb then + self:_render() + end + return { w = self._length, h = self._bb:getHeight() } +end + +function TextWidget:paintTo(bb, x, y) + if not self._bb then + self:_render() + end + bb:blitFrom(self._bb, x, y, 0, 0, self._length, self._bb:getHeight()) +end + +function TextWidget:free() + if self._bb then + self._bb:free() + self._bb = nil + end +end + +--[[ +ImageWidget shows an image from a file +]] +ImageWidget = Widget:new({ + file = nil, + _bb = nil +}) + +function ImageWidget:_render() + local itype = string.lower(string.match(self.file, ".+%.([^.]+)") or "") + if itype == "jpeg" or itype == "jpg" then + self._bb = Image.fromJPEG(self.file) + elseif itype == "png" then + self._bb = Image.fromPNG(self.file) + end +end + +function ImageWidget:getSize() + if not self._bb then + self:_render() + end + return { w = self._bb:getWidth(), h = self._bb:getHeight() } +end + +function ImageWidget:paintTo(bb, x, y) + local size = self:getSize() + bb:blitFrom(self._bb, x, y, 0, 0, size.w, size.h) +end + +function ImageWidget:free() + if self._bb then + self._bb:free() + self._bb = nil + end +end + +--[[ +A Layout widget that puts objects besides each others +]] +HorizontalGroup = WidgetContainer:new({ + align = "center", + _size = nil, +}) + +function HorizontalGroup:getSize() + if not self._size then + self._size = { w = 0, h = 0 } + self._offsets = { } + for i, widget in ipairs(self) do + local w_size = widget:getSize() + self._offsets[i] = { + x = self._size.w, + y = w_size.h + } + self._size.w = self._size.w + w_size.w + if w_size.h > self._size.h then + self._size.h = w_size.h + end + end + end + return self._size +end + +function HorizontalGroup:paintTo(bb, x, y) + local size = self:getSize() + + for i, widget in ipairs(self) do + if self.align == "center" then + widget:paintTo(bb, x + self._offsets[i].x, y + (size.h - self._offsets[i].y) / 2) + elseif self.align == "top" then + widget:paintTo(bb, x + self._offsets[i].x, y) + elseif self.align == "bottom" then + widget:paintTo(bb, x + self._offsets[i].x, y + size.h - self._offsets[i].y) + end + end +end + +function HorizontalGroup:free() + self._size = nil + self._offsets = {} + WidgetContainer.free(self) +end + +--[[ +A Layout widget that puts objects under each other +]] +VerticalGroup = WidgetContainer:new({ + align = "center", + _size = nil, + _offsets = {} +}) + +function VerticalGroup:getSize() + if not self._size then + self._size = { w = 0, h = 0 } + self._offsets = { } + for i, widget in ipairs(self) do + local w_size = widget:getSize() + self._offsets[i] = { + x = w_size.w, + y = self._size.h, + } + self._size.h = self._size.h + w_size.h + if w_size.w > self._size.w then + self._size.w = w_size.w + end + end + end + return self._size +end + +function VerticalGroup:paintTo(bb, x, y) + local size = self:getSize() + + for i, widget in ipairs(self) do + if self.align == "center" then + widget:paintTo(bb, x + (size.w - self._offsets[i].x) / 2, y + self._offsets[i].y) + elseif self.align == "left" then + widget:paintTo(bb, x, y + self._offsets[i].y) + elseif self.align == "right" then + widget:paintTo(bb, x + size.w - self._offsets[i].x, y + self._offsets[i].y) + end + end +end + +function VerticalGroup:free() + self._size = nil + self._offsets = {} + WidgetContainer.free(self) +end