Codebase list sugar-read-activity / 69769ee
Remove EPUB support EPUB support in Read is closely integrated with WebKit 3.0 API, based on WebKit 2.4, which is unmaintained and dangerous. This patch removes EPUB support; there is no longer a dependency on WebKit. James Cameron 6 years ago
13 changed file(s) with 3 addition(s) and 2006 deletion(s). Raw diff Collapse all Expand all
55 locale
66 MANIFEST
77 dist/
8 epubview/.svn/
33 icon = activity-read
44 exec = sugar-activity readactivity.ReadActivity
55 activity_version = 118
6 mime_types = application/pdf;image/vnd.djvu;image/x.djvu;image/tiff;application/epub+zip;text/plain;application/zip;application/x-cbz
6 mime_types = application/pdf;image/vnd.djvu;image/x.djvu;image/tiff;text/plain;application/zip;application/x-cbz
77 license = GPLv2+
88 summary = Use this activity when you are ready to read! Remember to flip your computer around to feel like you are really holding a book!
99 categories = language documents media system
+0
-7
activity/mimetypes.xml less more
0 <?xml version="1.0" encoding="UTF-8"?>
1 <mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
2 <mime-type type="application/epub+zip">
3 <comment xml:lang="en">Epub document</comment>
4 <glob pattern="*.epub"/>
5 </mime-type>
6 </mime-info>
+0
-309
epubadapter.py less more
0 from gi.repository import GObject
1 import logging
2
3 import epubview
4
5 # import speech
6
7 from cStringIO import StringIO
8
9 _logger = logging.getLogger('read-activity')
10
11
12 class EpubViewer(epubview.EpubView):
13
14 def __init__(self):
15 epubview.EpubView.__init__(self)
16
17 def setup(self, activity):
18 self.set_screen_dpi(activity.dpi)
19 self.connect('selection-changed',
20 activity._view_selection_changed_cb)
21
22 activity._hbox.pack_start(self, True, True, 0)
23 self.show_all()
24 self._modified_files = []
25
26 # text to speech initialization
27 self.current_word = 0
28 self.word_tuples = []
29
30 def load_document(self, file_path):
31 self.set_document(EpubDocument(self, file_path.replace('file://', '')))
32 # speech.highlight_cb = self.highlight_next_word
33 # speech.reset_cb = self.reset_text_to_speech
34 # speech.end_text_cb = self.get_more_text
35
36 def load_metadata(self, activity):
37
38 self.metadata = activity.metadata
39
40 if not self.metadata['title_set_by_user'] == '1':
41 title = self._epub._info._get_title()
42 if title:
43 self.metadata['title'] = title
44 if 'Read_zoom' in self.metadata:
45 try:
46 logging.error('Loading zoom %s', self.metadata['Read_zoom'])
47 self.set_zoom(float(self.metadata['Read_zoom']))
48 except:
49 pass
50
51 def update_metadata(self, activity):
52 self.metadata = activity.metadata
53 self.metadata['Read_zoom'] = self.get_zoom()
54
55 def zoom_to_width(self):
56 pass
57
58 def zoom_to_best_fit(self):
59 pass
60
61 def zoom_to_actual_size(self):
62 pass
63
64 def can_zoom_to_width(self):
65 return False
66
67 def can_highlight(self):
68 return True
69
70 def show_highlights(self, page):
71 # we save the highlights in the page as html
72 pass
73
74 def toggle_highlight(self, highlight):
75 self._view.set_editable(True)
76
77 if highlight:
78 self._view.execute_script(
79 'document.execCommand("backColor", false, "yellow");')
80 else:
81 # need remove the highlight nodes
82 js = """
83 var selObj = window.getSelection();
84 var range = selObj.getRangeAt(0);
85 var node = range.startContainer;
86 while (node.parentNode != null) {
87 if (node.localName == "span") {
88 if (node.hasAttributes()) {
89 var attrs = node.attributes;
90 for(var i = attrs.length - 1; i >= 0; i--) {
91 if (attrs[i].name == "style" &&
92 attrs[i].value == "background-color: yellow;") {
93 node.removeAttribute("style");
94 break;
95 };
96 };
97 };
98 };
99 node = node.parentNode;
100 };"""
101 self._view.execute_script(js)
102
103 self._view.set_editable(False)
104 # mark the file as modified
105 current_file = self.get_current_file()
106 logging.error('file %s was modified', current_file)
107 if current_file not in self._modified_files:
108 self._modified_files.append(current_file)
109 GObject.idle_add(self._save_page)
110
111 def _save_page(self):
112 oldtitle = self._view.get_title()
113 self._view.execute_script(
114 "document.title=document.documentElement.innerHTML;")
115 html = self._view.get_title()
116 file_path = self.get_current_file().replace('file:///', '/')
117 logging.error(html)
118 with open(file_path, 'w') as fd:
119 header = """<?xml version="1.0" encoding="utf-8" standalone="no"?>
120 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
121 "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
122 <html xmlns="http://www.w3.org/1999/xhtml">"""
123 fd.write(header)
124 fd.write(html)
125 fd.write('</html>')
126 self._view.execute_script('document.title=%s;' % oldtitle)
127
128 def save(self, file_path):
129 if self._modified_files:
130 self._epub.write(file_path)
131 return True
132
133 return False
134
135 def in_highlight(self):
136 # Verify if the selection already exist or the cursor
137 # is in a highlighted area
138 page_title = self._view.get_title()
139 js = """
140 var selObj = window.getSelection();
141 var range = selObj.getRangeAt(0);
142 var node = range.startContainer;
143 var onHighlight = false;
144 while (node.parentNode != null) {
145 if (node.localName == "span") {
146 if (node.hasAttributes()) {
147 var attrs = node.attributes;
148 for(var i = attrs.length - 1; i >= 0; i--) {
149 if (attrs[i].name == "style" &&
150 attrs[i].value == "background-color: yellow;") {
151 onHighlight = true;
152 };
153 };
154 };
155 };
156 node = node.parentNode;
157 };
158 document.title=onHighlight;"""
159 self._view.execute_script(js)
160 on_highlight = self._view.get_title() == 'true'
161 self._view.execute_script('document.title = "%s";' % page_title)
162 # the second parameter is only used in the text backend
163 return on_highlight, None
164
165 def can_do_text_to_speech(self):
166 return False
167
168 def can_rotate(self):
169 return False
170
171 def get_marked_words(self):
172 "Adds a mark between each word of text."
173 i = self.current_word
174 file_str = StringIO()
175 file_str.write('<speak> ')
176 end_range = i + 40
177 if end_range > len(self.word_tuples):
178 end_range = len(self.word_tuples)
179 for word_tuple in self.word_tuples[self.current_word:end_range]:
180 file_str.write('<mark name="' + str(i) + '"/>' +
181 word_tuple[2].encode('utf-8'))
182 i = i + 1
183 self.current_word = i
184 file_str.write('</speak>')
185 return file_str.getvalue()
186
187 def get_more_text(self):
188 pass
189 """
190 if self.current_word < len(self.word_tuples):
191 speech.stop()
192 more_text = self.get_marked_words()
193 speech.play(more_text)
194 else:
195 if speech.reset_buttons_cb is not None:
196 speech.reset_buttons_cb()
197 """
198
199 def reset_text_to_speech(self):
200 self.current_word = 0
201
202 def highlight_next_word(self, word_count):
203 pass
204 """
205 TODO: disabled because javascript can't be executed
206 with the velocity needed
207 self.current_word = word_count
208 self._view.highlight_next_word()
209 return True
210 """
211
212 def connect_zoom_handler(self, handler):
213 self._zoom_handler = handler
214 self._view_notify_zoom_handler = \
215 self.connect('notify::scale', handler)
216 return self._view_notify_zoom_handler
217
218 def connect_page_changed_handler(self, handler):
219 self.connect('page-changed', handler)
220
221 def _try_load_page(self, n):
222 if self._ready:
223 self._load_page(n)
224 return False
225 else:
226 return True
227
228 def set_screen_dpi(self, dpi):
229 return
230
231 def find_set_highlight_search(self, set_highlight_search):
232 self._view.set_highlight_text_matches(set_highlight_search)
233
234 def set_current_page(self, n):
235 # When the book is being loaded, calling this does not help
236 # In such a situation, we go into a loop and try to load the
237 # supplied page when the book has loaded completely
238 n += 1
239 if self._ready:
240 self._load_page(n)
241 else:
242 GObject.timeout_add(200, self._try_load_page, n)
243
244 def get_current_page(self):
245 return int(self._loaded_page) - 1
246
247 def get_current_link(self):
248 # the _loaded_filename include all the path,
249 # need only the part included in the link
250 return self._loaded_filename[len(self._epub._tempdir) + 1:]
251
252 def update_toc(self, activity):
253 if self._epub.has_document_links():
254 activity.show_navigator_button()
255 activity.set_navigator_model(self._epub.get_links_model())
256 return True
257 else:
258 return False
259
260 def get_link_iter(self, current_link):
261 """
262 Returns the iter related to a link
263 """
264 link_iter = self._epub.get_links_model().get_iter_first()
265
266 while link_iter is not None and \
267 self._epub.get_links_model().get_value(link_iter, 1) \
268 != current_link:
269 link_iter = self._epub.get_links_model().iter_next(link_iter)
270 return link_iter
271
272 def find_changed(self, job, page=None):
273 self._find_changed(job)
274
275 def handle_link(self, link):
276 self._load_file(link)
277
278 def setup_find_job(self, text, updated_cb):
279 self._find_job = JobFind(document=self._epub,
280 start_page=0, n_pages=self.get_pagecount(),
281 text=text, case_sensitive=False)
282 self._find_updated_handler = self._find_job.connect('updated',
283 updated_cb)
284 return self._find_job, self._find_updated_handler
285
286
287 class EpubDocument(epubview.Epub):
288
289 def __init__(self, view, docpath):
290 epubview.Epub.__init__(self, docpath)
291 self._page_cache = view
292
293 def get_n_pages(self):
294 return int(self._page_cache.get_pagecount())
295
296 def has_document_links(self):
297 return True
298
299 def get_links_model(self):
300 return self.get_toc_model()
301
302
303 class JobFind(epubview.JobFind):
304
305 def __init__(self, document, start_page, n_pages, text,
306 case_sensitive=False):
307 epubview.JobFind.__init__(self, document, start_page, n_pages, text,
308 case_sensitive=False)
+0
-20
epubview/__init__.py less more
0 # Copyright 2009 One Laptop Per Child
1 # Author: Sayamindu Dasgupta <sayamindu@laptop.org>
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16
17 from epub import _Epub as Epub
18 from epubview import _View as EpubView
19 from jobs import _JobFind as JobFind
+0
-202
epubview/epub.py less more
0 # Copyright 2009 One Laptop Per Child
1 # Author: Sayamindu Dasgupta <sayamindu@laptop.org>
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16
17 import zipfile
18 import tempfile
19 import os
20 import xml.etree.ElementTree as etree
21 import shutil
22 import logging
23
24 import navmap
25 import epubinfo
26
27
28 class _Epub(object):
29
30 def __init__(self, _file):
31 """
32 _file: can be either a path to a file (a string) or a file-like object.
33 """
34 self._file = _file
35 self._zobject = None
36 self._opfpath = None
37 self._ncxpath = None
38 self._basepath = None
39 self._tempdir = tempfile.mkdtemp()
40
41 if not self._verify():
42 print 'Warning: This does not seem to be a valid epub file'
43
44 self._get_opf()
45 self._get_ncx()
46
47 ncxfile = self._zobject.open(self._ncxpath)
48 opffile = self._zobject.open(self._opfpath)
49 self._navmap = navmap.NavMap(opffile, ncxfile, self._basepath)
50
51 opffile = self._zobject.open(self._opfpath)
52 self._info = epubinfo.EpubInfo(opffile)
53
54 self._unzip()
55
56 def _unzip(self):
57 # This is broken upto python 2.7
58 # self._zobject.extractall(path = self._tempdir)
59 orig_cwd = os.getcwd()
60 os.chdir(self._tempdir)
61 for name in self._zobject.namelist():
62 # Some weird zip file entries start with a slash,
63 # and we don't want to write to the root directory
64 try:
65 if name.startswith(os.path.sep):
66 name = name[1:]
67 if name.endswith(os.path.sep) or name.endswith('\\'):
68 os.makedirs(name)
69 except:
70 logging.error('ERROR unziping %s', name)
71 else:
72 self._zobject.extract(name)
73 os.chdir(orig_cwd)
74
75 def _get_opf(self):
76 containerfile = self._zobject.open('META-INF/container.xml')
77
78 tree = etree.parse(containerfile)
79 root = tree.getroot()
80
81 r_id = './/{urn:oasis:names:tc:opendocument:xmlns:container}rootfile'
82 for element in root.iterfind(r_id):
83 if element.get('media-type') == 'application/oebps-package+xml':
84 self._opfpath = element.get('full-path')
85
86 if self._opfpath.rpartition('/')[0]:
87 self._basepath = self._opfpath.rpartition('/')[0] + '/'
88 else:
89 self._basepath = ''
90
91 containerfile.close()
92
93 def _get_ncx(self):
94 opffile = self._zobject.open(self._opfpath)
95
96 tree = etree.parse(opffile)
97 root = tree.getroot()
98
99 spine = root.find('.//{http://www.idpf.org/2007/opf}spine')
100 tocid = spine.get('toc')
101
102 for element in root.iterfind('.//{http://www.idpf.org/2007/opf}item'):
103 if element.get('id') == tocid:
104 self._ncxpath = self._basepath + element.get('href')
105
106 opffile.close()
107
108 def _verify(self):
109 '''
110 Method to crudely check to verify that what we
111 are dealing with is a epub file or not
112 '''
113 if isinstance(self._file, basestring):
114 if not os.path.exists(self._file):
115 return False
116
117 self._zobject = zipfile.ZipFile(self._file)
118
119 if 'mimetype' not in self._zobject.namelist():
120 return False
121
122 mtypefile = self._zobject.open('mimetype')
123 mimetype = mtypefile.readline()
124
125 # Some files seem to have trailing characters
126 if not mimetype.startswith('application/epub+zip'):
127 return False
128
129 return True
130
131 def get_toc_model(self):
132 '''
133 Returns a GtkTreeModel representation of the
134 Epub table of contents
135 '''
136 return self._navmap.get_gtktreestore()
137
138 def get_flattoc(self):
139 '''
140 Returns a flat (linear) list of files to be
141 rendered.
142 '''
143 return self._navmap.get_flattoc()
144
145 def get_basedir(self):
146 '''
147 Returns the base directory where the contents of the
148 epub has been unzipped
149 '''
150 return self._tempdir
151
152 def get_info(self):
153 '''
154 Returns a EpubInfo object title
155 '''
156 return self._info.title
157
158 def write(self, file_path):
159 '''Create the ZIP archive.
160 The mimetype must be the first file in the archive
161 and it must not be compressed.'''
162
163 # The EPUB must contain the META-INF and mimetype files at the root, so
164 # we'll create the archive in the working directory first
165 # and move it later
166 current_dir = os.getcwd()
167 os.chdir(self._tempdir)
168
169 # Open a new zipfile for writing
170 epub = zipfile.ZipFile(file_path, 'w')
171
172 # Add the mimetype file first and set it to be uncompressed
173 epub.write('mimetype', compress_type=zipfile.ZIP_STORED)
174
175 # For the remaining paths in the EPUB, add all of their files
176 # using normal ZIP compression
177 self._scan_dir('.', epub)
178
179 epub.close()
180 os.chdir(current_dir)
181
182 def _scan_dir(self, path, epub_file):
183 for p in os.listdir(path):
184 logging.error('add file %s', p)
185 if os.path.isdir(os.path.join(path, p)):
186 self._scan_dir(os.path.join(path, p), epub_file)
187 else:
188 if p != 'mimetype':
189 epub_file.write(
190 os.path.join(path, p),
191 compress_type=zipfile.ZIP_DEFLATED)
192
193 def close(self):
194 '''
195 Cleans up (closes open zip files and deletes
196 uncompressed content of Epub.
197 Please call this when a file is being closed or during
198 application exit.
199 '''
200 self._zobject.close()
201 shutil.rmtree(self._tempdir)
+0
-114
epubview/epubinfo.py less more
0 import xml.etree.ElementTree as etree
1
2
3 class EpubInfo():
4
5 # TODO: Cover the entire DC range
6
7 def __init__(self, opffile):
8 self._tree = etree.parse(opffile)
9 self._root = self._tree.getroot()
10 self._e_metadata = self._root.find(
11 '{http://www.idpf.org/2007/opf}metadata')
12
13 self.title = self._get_title()
14 self.creator = self._get_creator()
15 self.date = self._get_date()
16 self.subject = self._get_subject()
17 self.source = self._get_source()
18 self.rights = self._get_rights()
19 self.identifier = self._get_identifier()
20 self.language = self._get_language()
21 self.summary = self._get_description()
22 self.cover_image = self._get_cover_image()
23
24 def _get_data(self, tagname):
25 element = self._e_metadata.find(tagname)
26 return element.text
27
28 def _get_title(self):
29 try:
30 ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}title')
31 except AttributeError:
32 return None
33
34 return ret
35
36 def _get_description(self):
37 try:
38 ret = self._get_data(
39 './/{http://purl.org/dc/elements/1.1/}description')
40 except AttributeError:
41 return None
42
43 return ret
44
45 def _get_creator(self):
46 try:
47 ret = self._get_data(
48 './/{http://purl.org/dc/elements/1.1/}creator')
49 except AttributeError:
50 return None
51 return ret
52
53 def _get_date(self):
54 # TODO: iter
55 try:
56 ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}date')
57 except AttributeError:
58 return None
59
60 return ret
61
62 def _get_source(self):
63 try:
64 ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}source')
65 except AttributeError:
66 return None
67
68 return ret
69
70 def _get_rights(self):
71 try:
72 ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}rights')
73 except AttributeError:
74 return None
75
76 return ret
77
78 def _get_identifier(self):
79 # TODO: iter
80 element = self._e_metadata.find(
81 './/{http://purl.org/dc/elements/1.1/}identifier')
82
83 if element is not None:
84 return {'id': element.get('id'), 'value': element.text}
85 else:
86 return None
87
88 def _get_language(self):
89 try:
90 ret = self._get_data(
91 './/{http://purl.org/dc/elements/1.1/}language')
92 except AttributeError:
93 return None
94
95 return ret
96
97 def _get_subject(self):
98 try:
99 subjectlist = []
100 for element in self._e_metadata.iterfind(
101 './/{http://purl.org/dc/elements/1.1/}subject'):
102 subjectlist.append(element.text)
103 except AttributeError:
104 return None
105
106 return subjectlist
107
108 def _get_cover_image(self):
109 element = self._e_metadata.find('{http://www.idpf.org/2007/opf}meta')
110 if element is not None and element.get('name') == 'cover':
111 return element.get('content')
112 else:
113 return None
+0
-712
epubview/epubview.py less more
0 # Copyright 2009 One Laptop Per Child
1 # Author: Sayamindu Dasgupta <sayamindu@laptop.org>
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16
17 from gi.repository import Gtk
18 from gi.repository import GObject
19 from gi.repository import Gdk
20 import widgets
21
22 import logging
23 import os.path
24 import math
25 import shutil
26
27 from jobs import _JobPaginator as _Paginator
28
29 LOADING_HTML = '''
30 <div style="width:100%;height:100%;text-align:center;padding-top:50%;">
31 <h1>Loading...</h1>
32 </div>
33 '''
34
35
36 class _View(Gtk.HBox):
37
38 __gproperties__ = {
39 'scale': (GObject.TYPE_FLOAT, 'the zoom level',
40 'the zoom level of the widget',
41 0.5, 4.0, 1.0, GObject.PARAM_READWRITE),
42 }
43 __gsignals__ = {
44 'page-changed': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
45 ([int, int])),
46 'selection-changed': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
47 ([])),
48 }
49
50 def __init__(self):
51 GObject.threads_init()
52 Gtk.HBox.__init__(self)
53
54 self.connect("destroy", self._destroy_cb)
55
56 self._ready = False
57 self._paginator = None
58 self._loaded_page = -1
59 self._file_loaded = True
60 # self._old_scrollval = -1
61 self._loaded_filename = None
62 self._pagecount = -1
63 self.__going_fwd = True
64 self.__going_back = False
65 self.__page_changed = False
66 self._has_selection = False
67 self.scale = 1.0
68 self._epub = None
69 self._findjob = None
70 self.__in_search = False
71 self.__search_fwd = True
72 self._filelist = None
73 self._internal_link = None
74
75 self._sw = Gtk.ScrolledWindow()
76 self._view = widgets._WebView()
77 self._view.load_string(LOADING_HTML, 'text/html', 'utf-8', '/')
78 settings = self._view.get_settings()
79 settings.props.default_font_family = 'DejaVu LGC Serif'
80 settings.props.enable_plugins = False
81 settings.props.default_encoding = 'utf-8'
82 self._view.connect('load-finished', self._view_load_finished_cb)
83 self._view.connect('scroll-event', self._view_scroll_event_cb)
84 self._view.connect('key-press-event', self._view_keypress_event_cb)
85 self._view.connect('selection-changed',
86 self._view_selection_changed_cb)
87 self._view.connect_after('populate-popup',
88 self._view_populate_popup_cb)
89 self._view.connect('touch-change-page', self.__touch_page_changed_cb)
90
91 self._sw.add(self._view)
92 self._v_vscrollbar = self._sw.get_vscrollbar()
93 self._v_scrollbar_value_changed_cb_id = self._v_vscrollbar.connect(
94 'value-changed', self._v_scrollbar_value_changed_cb)
95 self._scrollbar = Gtk.VScrollbar()
96 self._scrollbar_change_value_cb_id = self._scrollbar.connect(
97 'change-value', self._scrollbar_change_value_cb)
98
99 overlay = Gtk.Overlay()
100 hbox = Gtk.HBox()
101 overlay.add(hbox)
102 hbox.add(self._sw)
103
104 self._scrollbar.props.halign = Gtk.Align.END
105 self._scrollbar.props.valign = Gtk.Align.FILL
106 overlay.add_overlay(self._scrollbar)
107
108 self.pack_start(overlay, True, True, 0)
109
110 self._view.set_can_default(True)
111 self._view.set_can_focus(True)
112
113 def map_cp(widget):
114 widget.setup_touch()
115 widget.disconnect(self._setup_handle)
116
117 self._setup_handle = self._view.connect('map', map_cp)
118
119 def set_document(self, epubdocumentinstance):
120 '''
121 Sets document (should be a Epub instance)
122 '''
123 self._epub = epubdocumentinstance
124 GObject.idle_add(self._paginate)
125
126 def do_get_property(self, property):
127 if property.name == 'has-selection':
128 return self._has_selection
129 elif property.name == 'scale':
130 return self.scale
131 else:
132 raise AttributeError('unknown property %s' % property.name)
133
134 def do_set_property(self, property, value):
135 if property.name == 'scale':
136 self.__set_zoom(value)
137 else:
138 raise AttributeError('unknown property %s' % property.name)
139
140 def get_has_selection(self):
141 '''
142 Returns True if any part of the content is selected
143 '''
144 return self._view.can_copy_clipboard()
145
146 def get_zoom(self):
147 '''
148 Returns the current zoom level
149 '''
150 return self.get_property('scale') * 100.0
151
152 def set_zoom(self, value):
153 '''
154 Sets the current zoom level
155 '''
156 scrollbar_pos = self.get_vertical_pos()
157 self._view.set_zoom_level(value / 100.0)
158 self.set_vertical_pos(scrollbar_pos)
159
160 def _get_scale(self):
161 '''
162 Returns the current zoom level
163 '''
164 return self.get_property('scale')
165
166 def _set_scale(self, value):
167 '''
168 Sets the current zoom level
169 '''
170 self.set_property('scale', value)
171
172 def zoom_in(self):
173 '''
174 Zooms in (increases zoom level by 0.1)
175 '''
176 if self.can_zoom_in():
177 scrollbar_pos = self.get_vertical_pos()
178 self._set_scale(self._get_scale() + 0.1)
179 self.set_vertical_pos(scrollbar_pos)
180 return True
181 else:
182 return False
183
184 def zoom_out(self):
185 '''
186 Zooms out (decreases zoom level by 0.1)
187 '''
188 if self.can_zoom_out():
189 scrollbar_pos = self.get_vertical_pos()
190 self._set_scale(self._get_scale() - 0.1)
191 self.set_vertical_pos(scrollbar_pos)
192 return True
193 else:
194 return False
195
196 def get_vertical_pos(self):
197 """
198 Used to save the scrolled position and restore when needed
199 """
200 return self._v_vscrollbar.get_adjustment().get_value()
201
202 def set_vertical_pos(self, position):
203 """
204 Used to save the scrolled position and restore when needed
205 """
206 self._v_vscrollbar.get_adjustment().set_value(position)
207
208 def can_zoom_in(self):
209 '''
210 Returns True if it is possible to zoom in further
211 '''
212 if self.scale < 4:
213 return True
214 else:
215 return False
216
217 def can_zoom_out(self):
218 '''
219 Returns True if it is possible to zoom out further
220 '''
221 if self.scale > 0.5:
222 return True
223 else:
224 return False
225
226 def get_current_page(self):
227 '''
228 Returns the currently loaded page
229 '''
230 return self._loaded_page
231
232 def get_current_file(self):
233 '''
234 Returns the currently loaded XML file
235 '''
236 # return self._loaded_filename
237 if self._paginator:
238 return self._paginator.get_file_for_pageno(self._loaded_page)
239 else:
240 return None
241
242 def get_pagecount(self):
243 '''
244 Returns the pagecount of the loaded file
245 '''
246 return self._pagecount
247
248 def set_current_page(self, n):
249 '''
250 Loads page number n
251 '''
252 if n < 1 or n > self._pagecount:
253 return False
254 self._load_page(n)
255 return True
256
257 def next_page(self):
258 '''
259 Loads next page if possible
260 Returns True if transition to next page is possible and done
261 '''
262 if self._loaded_page == self._pagecount:
263 return False
264 self._load_next_page()
265 return True
266
267 def previous_page(self):
268 '''
269 Loads previous page if possible
270 Returns True if transition to previous page is possible and done
271 '''
272 if self._loaded_page == 1:
273 return False
274 self._load_prev_page()
275 return True
276
277 def scroll(self, scrolltype, horizontal):
278 '''
279 Scrolls through the pages.
280 Scrolling is horizontal if horizontal is set to True
281 Valid scrolltypes are:
282 Gtk.ScrollType.PAGE_BACKWARD, Gtk.ScrollType.PAGE_FORWARD,
283 Gtk.ScrollType.STEP_BACKWARD, Gtk.ScrollType.STEP_FORWARD
284 Gtk.ScrollType.STEP_START and Gtk.ScrollType.STEP_STOP
285 '''
286 if scrolltype == Gtk.ScrollType.PAGE_BACKWARD:
287 self.__going_back = True
288 self.__going_fwd = False
289 if not self._do_page_transition():
290 self._view.move_cursor(Gtk.MovementStep.PAGES, -1)
291 elif scrolltype == Gtk.ScrollType.PAGE_FORWARD:
292 self.__going_back = False
293 self.__going_fwd = True
294 if not self._do_page_transition():
295 self._view.move_cursor(Gtk.MovementStep.PAGES, 1)
296 elif scrolltype == Gtk.ScrollType.STEP_BACKWARD:
297 self.__going_fwd = False
298 self.__going_back = True
299 if not self._do_page_transition():
300 self._view.move_cursor(Gtk.MovementStep.DISPLAY_LINES, -1)
301 elif scrolltype == Gtk.ScrollType.STEP_FORWARD:
302 self.__going_fwd = True
303 self.__going_back = False
304 if not self._do_page_transition():
305 self._view.move_cursor(Gtk.MovementStep.DISPLAY_LINES, 1)
306 elif scrolltype == Gtk.ScrollType.START:
307 self.__going_back = True
308 self.__going_fwd = False
309 if not self._do_page_transition():
310 self.set_current_page(1)
311 elif scrolltype == Gtk.ScrollType.END:
312 self.__going_back = False
313 self.__going_fwd = True
314 if not self._do_page_transition():
315 self.set_current_page(self._pagecount - 1)
316 else:
317 print ('Got unsupported scrolltype %s' % str(scrolltype))
318
319 def __touch_page_changed_cb(self, widget, forward):
320 if forward:
321 self.scroll(Gtk.ScrollType.PAGE_FORWARD, False)
322 else:
323 self.scroll(Gtk.ScrollType.PAGE_BACKWARD, False)
324
325 def copy(self):
326 '''
327 Copies the current selection to clipboard.
328 '''
329 self._view.copy_clipboard()
330
331 def find_next(self):
332 '''
333 Highlights the next matching item for current search
334 '''
335 self._view.grab_focus()
336
337 if self._view.search_text(self._findjob.get_search_text(),
338 self._findjob.get_case_sensitive(),
339 True, False):
340 return
341 else:
342 path = os.path.join(self._epub.get_basedir(),
343 self._findjob.get_next_file())
344 self.__in_search = True
345 self.__search_fwd = True
346 self._load_file(path)
347
348 def find_previous(self):
349 '''
350 Highlights the previous matching item for current search
351 '''
352 self._view.grab_focus()
353
354 if self._view.search_text(self._findjob.get_search_text(),
355 self._findjob.get_case_sensitive(),
356 False, False):
357 return
358 else:
359 path = os.path.join(self._epub.get_basedir(),
360 self._findjob.get_prev_file())
361 self.__in_search = True
362 self.__search_fwd = False
363 self._load_file(path)
364
365 def _find_changed(self, job):
366 self._view.grab_focus()
367 self._findjob = job
368 self._mark_found_text()
369 self.find_next()
370
371 def _mark_found_text(self):
372 self._view.unmark_text_matches()
373 self._view.mark_text_matches(
374 self._findjob.get_search_text(),
375 case_sensitive=self._findjob.get_case_sensitive(), limit=0)
376 self._view.set_highlight_text_matches(True)
377
378 def __set_zoom(self, value):
379 self._view.set_zoom_level(value)
380 self.scale = value
381
382 def _view_populate_popup_cb(self, view, menu):
383 menu.destroy() # HACK
384 return
385
386 def _view_selection_changed_cb(self, view):
387 self.emit('selection-changed')
388
389 def _view_keypress_event_cb(self, view, event):
390 name = Gdk.keyval_name(event.keyval)
391 if name == 'Page_Down' or name == 'Down':
392 self.__going_back = False
393 self.__going_fwd = True
394 elif name == 'Page_Up' or name == 'Up':
395 self.__going_back = True
396 self.__going_fwd = False
397
398 self._do_page_transition()
399
400 def _view_scroll_event_cb(self, view, event):
401 if event.direction == Gdk.ScrollDirection.DOWN:
402 self.__going_back = False
403 self.__going_fwd = True
404 elif event.direction == Gdk.ScrollDirection.UP:
405 self.__going_back = True
406 self.__going_fwd = False
407
408 self._do_page_transition()
409
410 def _do_page_transition(self):
411 if self.__going_fwd:
412 if self._v_vscrollbar.get_value() >= \
413 self._v_vscrollbar.props.adjustment.props.upper - \
414 self._v_vscrollbar.props.adjustment.props.page_size:
415 self._load_page(self._loaded_page + 1)
416 return True
417 elif self.__going_back:
418 if self._v_vscrollbar.get_value() == \
419 self._v_vscrollbar.props.adjustment.props.lower:
420 self._load_page(self._loaded_page - 1)
421 return True
422
423 return False
424
425 def _view_load_finished_cb(self, v, frame):
426
427 self._file_loaded = True
428 filename = self._view.props.uri.replace('file://', '')
429 if os.path.exists(filename.replace('xhtml', 'xml')):
430 # Hack for making javascript work
431 filename = filename.replace('xhtml', 'xml')
432
433 filename = filename.split('#')[0] # Get rid of anchors
434
435 if self._loaded_page < 1 or filename is None:
436 return False
437
438 self._loaded_filename = filename
439
440 remfactor = self._paginator.get_remfactor_for_file(filename)
441 pages = self._paginator.get_pagecount_for_file(filename)
442 extra = int(math.ceil(
443 remfactor * self._view.get_page_height() / (pages - remfactor)))
444 if extra > 0:
445 self._view.add_bottom_padding(extra)
446
447 if self.__in_search:
448 self._mark_found_text()
449 self._view.search_text(self._findjob.get_search_text(),
450 self._findjob.get_case_sensitive(),
451 self.__search_fwd, False)
452 self.__in_search = False
453 else:
454 if self.__going_back:
455 # We need to scroll to the last page
456 self._scroll_page_end()
457 else:
458 self._scroll_page()
459
460 # process_file = True
461 if self._internal_link is not None:
462 self._view.go_to_link(self._internal_link)
463 vertical_pos = \
464 self._view.get_vertical_position_element(self._internal_link)
465 # set the page number based in the vertical position
466 initial_page = self._paginator.get_base_pageno_for_file(filename)
467 self._loaded_page = initial_page + int(
468 vertical_pos / self._paginator.get_single_page_height())
469
470 # There are epub files, created with Calibre,
471 # where the link in the index points to the end of the previos
472 # file to the needed chapter.
473 # if the link is at the bottom of the page, we open the next file
474 one_page_height = self._paginator.get_single_page_height()
475 self._internal_link = None
476 if vertical_pos > self._view.get_page_height() - one_page_height:
477 logging.error('bottom page link, go to next file')
478 next_file = self._paginator.get_next_filename(filename)
479 if next_file is not None:
480 logging.error('load next file %s', next_file)
481 self.__in_search = False
482 self.__going_back = False
483 # process_file = False
484 GObject.idle_add(self._load_file, next_file)
485
486 # if process_file:
487 # # prepare text to speech
488 # html_file = open(self._loaded_filename)
489 # soup = BeautifulSoup.BeautifulSoup(html_file)
490 # body = soup.find('body')
491 # tags = body.findAll(text=True)
492 # self._all_text = ''.join([tag for tag in tags])
493 # self._prepare_text_to_speech(self._all_text)
494
495 def _prepare_text_to_speech(self, page_text):
496 i = 0
497 j = 0
498 word_begin = 0
499 word_end = 0
500 ignore_chars = [' ', '\n', u'\r', '_', '[', '{', ']', '}', '|',
501 '<', '>', '*', '+', '/', '\\']
502 ignore_set = set(ignore_chars)
503 self.word_tuples = []
504 len_page_text = len(page_text)
505 while i < len_page_text:
506 if page_text[i] not in ignore_set:
507 word_begin = i
508 j = i
509 while j < len_page_text and page_text[j] not in ignore_set:
510 j = j + 1
511 word_end = j
512 i = j
513 word_tuple = (word_begin, word_end,
514 page_text[word_begin: word_end])
515 if word_tuple[2] != u'\r':
516 self.word_tuples.append(word_tuple)
517 i = i + 1
518
519 def _scroll_page_end(self):
520 v_upper = self._v_vscrollbar.props.adjustment.props.upper
521 # v_page_size = self._v_vscrollbar.props.adjustment.props.page_size
522 self._v_vscrollbar.set_value(v_upper)
523
524 def _scroll_page(self):
525 pageno = self._loaded_page
526
527 v_upper = self._v_vscrollbar.props.adjustment.props.upper
528 v_page_size = self._v_vscrollbar.props.adjustment.props.page_size
529
530 scrollfactor = self._paginator.get_scrollfactor_pos_for_pageno(pageno)
531 self._v_vscrollbar.set_value((v_upper - v_page_size) * scrollfactor)
532
533 def _paginate(self):
534 filelist = []
535 for i in self._epub._navmap.get_flattoc():
536 filelist.append(os.path.join(self._epub._tempdir, i))
537 # init files info
538 self._filelist = filelist
539 self._paginator = _Paginator(filelist)
540 self._paginator.connect('paginated', self._paginated_cb)
541
542 def get_filelist(self):
543 return self._filelist
544
545 def get_tempdir(self):
546 return self._epub._tempdir
547
548 def _load_next_page(self):
549 self._load_page(self._loaded_page + 1)
550
551 def _load_prev_page(self):
552 self._load_page(self._loaded_page - 1)
553
554 def _v_scrollbar_value_changed_cb(self, scrollbar):
555 if self._loaded_page < 1:
556 return
557 scrollval = scrollbar.get_value()
558 scroll_upper = self._v_vscrollbar.props.adjustment.props.upper
559 scroll_page_size = self._v_vscrollbar.props.adjustment.props.page_size
560
561 if self.__going_fwd and not self._loaded_page == self._pagecount:
562 if self._paginator.get_file_for_pageno(self._loaded_page) != \
563 self._paginator.get_file_for_pageno(self._loaded_page + 1):
564 # We don't need this if the next page is in another file
565 return
566
567 scrollfactor_next = \
568 self._paginator.get_scrollfactor_pos_for_pageno(
569 self._loaded_page + 1)
570 if scrollval > 0:
571 scrollfactor = scrollval / (scroll_upper - scroll_page_size)
572 else:
573 scrollfactor = 0
574 if scrollfactor >= scrollfactor_next:
575 self._on_page_changed(self._loaded_page, self._loaded_page + 1)
576 elif self.__going_back and self._loaded_page > 1:
577 if self._paginator.get_file_for_pageno(self._loaded_page) != \
578 self._paginator.get_file_for_pageno(self._loaded_page - 1):
579 return
580
581 scrollfactor_cur = \
582 self._paginator.get_scrollfactor_pos_for_pageno(
583 self._loaded_page)
584 if scrollval > 0:
585 scrollfactor = scrollval / (scroll_upper - scroll_page_size)
586 else:
587 scrollfactor = 0
588
589 if scrollfactor <= scrollfactor_cur:
590 self._on_page_changed(self._loaded_page, self._loaded_page - 1)
591
592 def _on_page_changed(self, oldpage, pageno):
593 if oldpage == pageno:
594 return
595 self.__page_changed = True
596 self._loaded_page = pageno
597 self._scrollbar.handler_block(self._scrollbar_change_value_cb_id)
598 self._scrollbar.set_value(pageno)
599 self._scrollbar.handler_unblock(self._scrollbar_change_value_cb_id)
600 # the indexes in read activity are zero based
601 self.emit('page-changed', (oldpage - 1), (pageno - 1))
602
603 def _load_page(self, pageno):
604 if pageno > self._pagecount or pageno < 1:
605 # TODO: Cause an exception
606 return
607 if self._loaded_page == pageno:
608 return
609
610 filename = self._paginator.get_file_for_pageno(pageno)
611 filename = filename.replace('file://', '')
612
613 if filename != self._loaded_filename:
614 self._loaded_filename = filename
615 if not self._file_loaded:
616 # wait until the file is loaded
617 return
618 self._file_loaded = False
619
620 """
621 TODO: disabled because javascript can't be executed
622 with the velocity needed
623 # Copy javascript to highligth text to speech
624 destpath, destname = os.path.split(filename.replace('file://', ''))
625 shutil.copy('./epubview/highlight_words.js', destpath)
626 self._insert_js_reference(filename.replace('file://', ''),
627 destpath)
628 IMPORTANT: Find a way to do this without modify the files
629 now text highlight is implemented and the epub file is saved
630 """
631
632 if filename.endswith('xml'):
633 dest = filename.replace('xml', 'xhtml')
634 if not os.path.exists(dest):
635 os.symlink(filename, dest)
636 self._view.load_uri('file://' + dest)
637 else:
638 self._view.load_uri('file://' + filename)
639 else:
640 self._loaded_page = pageno
641 self._scroll_page()
642 self._on_page_changed(self._loaded_page, pageno)
643
644 def _insert_js_reference(self, file_name, path):
645 js_reference = '<script type="text/javascript" ' + \
646 'src="./highlight_words.js"></script>'
647 o = open(file_name + '.tmp', 'a')
648 for line in open(file_name):
649 line = line.replace('</head>', js_reference + '</head>')
650 o.write(line + "\n")
651 o.close()
652 shutil.copy(file_name + '.tmp', file_name)
653
654 def _load_file(self, path):
655 self._internal_link = None
656 if path.find('#') > -1:
657 self._internal_link = path[path.find('#'):]
658 path = path[:path.find('#')]
659
660 for filepath in self._filelist:
661 if filepath.endswith(path):
662 self._view.load_uri('file://' + filepath)
663 oldpage = self._loaded_page
664 self._loaded_page = \
665 self._paginator.get_base_pageno_for_file(filepath)
666 self._scroll_page()
667 self._on_page_changed(oldpage, self._loaded_page)
668 break
669
670 def _scrollbar_change_value_cb(self, range, scrolltype, value):
671 if scrolltype == Gtk.ScrollType.STEP_FORWARD:
672 self.__going_fwd = True
673 self.__going_back = False
674 if not self._do_page_transition():
675 self._view.move_cursor(Gtk.MovementStep.DISPLAY_LINES, 1)
676 elif scrolltype == Gtk.ScrollType.STEP_BACKWARD:
677 self.__going_fwd = False
678 self.__going_back = True
679 if not self._do_page_transition():
680 self._view.move_cursor(Gtk.MovementStep.DISPLAY_LINES, -1)
681 elif scrolltype == Gtk.ScrollType.JUMP or \
682 scrolltype == Gtk.ScrollType.PAGE_FORWARD or \
683 scrolltype == Gtk.ScrollType.PAGE_BACKWARD:
684 if value > self._scrollbar.props.adjustment.props.upper:
685 self._load_page(self._pagecount)
686 else:
687 self._load_page(round(value))
688 else:
689 print 'Warning: unknown scrolltype %s with value %f' \
690 % (str(scrolltype), value)
691
692 # FIXME: This should not be needed here
693 self._scrollbar.set_value(self._loaded_page)
694
695 if self.__page_changed:
696 self.__page_changed = False
697 return False
698 else:
699 return True
700
701 def _paginated_cb(self, object):
702 self._ready = True
703
704 self._pagecount = self._paginator.get_total_pagecount()
705 self._scrollbar.set_range(1.0, self._pagecount - 1.0)
706 self._scrollbar.set_increments(1.0, 1.0)
707 self._view.grab_focus()
708 self._view.grab_default()
709
710 def _destroy_cb(self, widget):
711 self._epub.close()
+0
-89
epubview/highlight_words.js less more
0 var parentElement;
1 var actualChild;
2 var actualWord;
3 var words;
4 var originalNode = null;
5 var modifiedNode = null;
6
7 function trim(s) {
8 s = ( s || '' ).replace( /^\s+|\s+$/g, '' );
9 return s.replace(/[\n\r\t]/g,' ');
10 }
11
12
13 function init() {
14 parentElement = document.getElementsByTagName("body")[0];
15 actualChild = new Array();
16 actualWord = 0;
17 actualChild.push(0);
18 }
19
20 function highLightNextWordInt() {
21 var nodeList = parentElement.childNodes;
22 ini_posi = actualChild[actualChild.length - 1];
23 for (var i=ini_posi; i < nodeList.length; i++) {
24 var node = nodeList[i];
25 if ((node.nodeName == "#text") && (trim(node.nodeValue) != '')) {
26 node_text = trim(node.nodeValue);
27 words = node_text.split(" ");
28 if (actualWord < words.length) {
29 originalNode = document.createTextNode(node.nodeValue);
30
31 prev_text = '';
32 for (var p1 = 0; p1 < actualWord; p1++) {
33 prev_text = prev_text + words[p1] + " ";
34 }
35 var textNode1 = document.createTextNode(prev_text);
36 var textNode2 = document.createTextNode(words[actualWord]+" ");
37 post_text = '';
38 for (var p2 = actualWord + 1; p2 < words.length; p2++) {
39 post_text = post_text + words[p2] + " ";
40 }
41 var textNode3 = document.createTextNode(post_text);
42 var newParagraph = document.createElement('p');
43 var boldNode = document.createElement('b');
44 boldNode.appendChild(textNode2);
45 newParagraph.appendChild(textNode1);
46 newParagraph.appendChild(boldNode);
47 newParagraph.appendChild(textNode3);
48
49 parentElement.insertBefore(newParagraph, node);
50 parentElement.removeChild(node);
51 modifiedNode = newParagraph;
52
53 actualWord = actualWord + 1;
54 if (actualWord >= words.length) {
55 actualChild.pop();
56 actualChild[actualChild.length - 1] = actualChild[actualChild.length - 1] + 2;
57 actualWord = 0;
58 parentElement = parentElement.parentNode;
59 }
60 }
61 throw "exit";
62 } else {
63 if (node.childNodes.length > 0) {
64 parentElement = node;
65 actualChild.push(0);
66 actualWord = 0;
67 highLightNextWordInt();
68 actualChild.pop();
69 }
70 }
71 }
72 return;
73 }
74
75
76 function highLightNextWord() {
77 if (typeof parentElement == "undefined") {
78 init();
79 }
80 if (originalNode != null) {
81 modifiedNode.parentNode.insertBefore(originalNode, modifiedNode);
82 modifiedNode.parentNode.removeChild(modifiedNode);
83 }
84 try {
85 highLightNextWordInt();
86 } catch(er) {
87 }
88 }
+0
-327
epubview/jobs.py less more
0 # Copyright 2009 One Laptop Per Child
1 # Author: Sayamindu Dasgupta <sayamindu@laptop.org>
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16
17
18 from gi.repository import GObject
19 from gi.repository import Gtk
20 import widgets
21 import math
22 import os.path
23 import xml.etree.ElementTree as etree
24
25 import threading
26
27 PAGE_WIDTH = 135
28 PAGE_HEIGHT = 216
29
30
31 def _pixel_to_mm(pixel, dpi):
32 inches = pixel / dpi
33 return int(inches / 0.03937)
34
35
36 def _mm_to_pixel(mm, dpi):
37 inches = mm * 0.03937
38 return int(inches * dpi)
39
40
41 class SearchThread(threading.Thread):
42
43 def __init__(self, obj):
44 threading.Thread.__init__(self)
45 self.obj = obj
46 self.stopthread = threading.Event()
47
48 def _start_search(self):
49 for entry in self.obj.flattoc:
50 if self.stopthread.isSet():
51 break
52 filepath = os.path.join(self.obj._document.get_basedir(), entry)
53 f = open(filepath)
54 if self._searchfile(f):
55 self.obj._matchfilelist.append(entry)
56 f.close()
57
58 self.obj._finished = True
59 GObject.idle_add(self.obj.emit, 'updated')
60
61 return False
62
63 def _searchfile(self, fileobj):
64 tree = etree.parse(fileobj)
65 root = tree.getroot()
66
67 body = None
68 for child in root:
69 if child.tag.endswith('body'):
70 body = child
71
72 if body is None:
73 return False
74
75 for child in body.iter():
76 if child.text is not None:
77 if child.text.lower().find(self.obj._text.lower()) > -1:
78 return True
79
80 return False
81
82 def run(self):
83 self._start_search()
84
85 def stop(self):
86 self.stopthread.set()
87
88
89 class _JobPaginator(GObject.GObject):
90
91 __gsignals__ = {
92 'paginated': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ([])),
93 }
94
95 def __init__(self, filelist):
96 GObject.GObject.__init__(self)
97
98 self._filelist = filelist
99 self._filedict = {}
100 self._pagemap = {}
101
102 self._bookheight = 0
103 self._count = 0
104 self._pagecount = 0
105
106 # TODO
107 """
108 self._screen = Gdk.Screen.get_default()
109 self._old_fontoptions = self._screen.get_font_options()
110 options = cairo.FontOptions()
111 options.set_hint_style(cairo.HINT_STYLE_MEDIUM)
112 options.set_antialias(cairo.ANTIALIAS_GRAY)
113 options.set_subpixel_order(cairo.SUBPIXEL_ORDER_DEFAULT)
114 options.set_hint_metrics(cairo.HINT_METRICS_DEFAULT)
115 self._screen.set_font_options(options)
116 """
117
118 self._temp_win = Gtk.Window()
119 self._temp_view = widgets._WebView(only_to_measure=True)
120
121 settings = self._temp_view.get_settings()
122 settings.props.default_font_family = 'DejaVu LGC Serif'
123 settings.props.sans_serif_font_family = 'DejaVu LGC Sans'
124 settings.props.serif_font_family = 'DejaVu LGC Serif'
125 settings.props.monospace_font_family = 'DejaVu LGC Sans Mono'
126 settings.props.enforce_96_dpi = True
127 # FIXME: This does not seem to work
128 # settings.props.auto_shrink_images = False
129 settings.props.enable_plugins = False
130 settings.props.default_font_size = 12
131 settings.props.default_monospace_font_size = 10
132 settings.props.default_encoding = 'utf-8'
133
134 sw = Gtk.ScrolledWindow()
135 sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
136 self._dpi = 96
137 self._single_page_height = _mm_to_pixel(PAGE_HEIGHT, self._dpi)
138 sw.set_size_request(_mm_to_pixel(PAGE_WIDTH, self._dpi),
139 self._single_page_height)
140 sw.add(self._temp_view)
141 self._temp_win.add(sw)
142 self._temp_view.connect('load-finished', self._page_load_finished_cb)
143
144 self._temp_win.show_all()
145 self._temp_win.unmap()
146
147 self._temp_view.open(self._filelist[self._count])
148
149 def get_single_page_height(self):
150 """
151 Returns the height in pixels of a single page
152 """
153 return self._single_page_height
154
155 def get_next_filename(self, actual_filename):
156 for n in range(len(self._filelist)):
157 filename = self._filelist[n]
158 if filename == actual_filename:
159 if n < len(self._filelist):
160 return self._filelist[n + 1]
161 return None
162
163 def _page_load_finished_cb(self, v, frame):
164 f = v.get_main_frame()
165 pageheight = v.get_page_height()
166
167 if pageheight <= self._single_page_height:
168 pages = 1
169 else:
170 pages = pageheight / float(self._single_page_height)
171 for i in range(1, int(math.ceil(pages) + 1)):
172 if pages - i < 0:
173 pagelen = (pages - math.floor(pages)) / pages
174 else:
175 pagelen = 1 / pages
176 self._pagemap[float(self._pagecount + i)] = \
177 (f.props.uri, (i - 1) / math.ceil(pages), pagelen)
178
179 self._pagecount += int(math.ceil(pages))
180 self._filedict[f.props.uri.replace('file://', '')] = \
181 (math.ceil(pages), math.ceil(pages) - pages)
182 self._bookheight += pageheight
183
184 if self._count + 1 >= len(self._filelist):
185 # TODO
186 # self._screen.set_font_options(self._old_fontoptions)
187 self.emit('paginated')
188 GObject.idle_add(self._cleanup)
189 else:
190 self._count += 1
191 self._temp_view.open(self._filelist[self._count])
192
193 def _cleanup(self):
194 self._temp_win.destroy()
195
196 def get_file_for_pageno(self, pageno):
197 '''
198 Returns the file in which pageno occurs
199 '''
200 return self._pagemap[pageno][0]
201
202 def get_scrollfactor_pos_for_pageno(self, pageno):
203 '''
204 Returns the position scrollfactor (fraction) for pageno
205 '''
206 return self._pagemap[pageno][1]
207
208 def get_scrollfactor_len_for_pageno(self, pageno):
209 '''
210 Returns the length scrollfactor (fraction) for pageno
211 '''
212 return self._pagemap[pageno][2]
213
214 def get_pagecount_for_file(self, filename):
215 '''
216 Returns the number of pages in file
217 '''
218 return self._filedict[filename][0]
219
220 def get_base_pageno_for_file(self, filename):
221 '''
222 Returns the pageno which begins in filename
223 '''
224 for key in self._pagemap.keys():
225 if self._pagemap[key][0].replace('file://', '') == filename:
226 return key
227
228 return None
229
230 def get_remfactor_for_file(self, filename):
231 '''
232 Returns the remainder
233 factor (1 - fraction length of last page in file)
234 '''
235 return self._filedict[filename][1]
236
237 def get_total_pagecount(self):
238 '''
239 Returns the total pagecount for the Epub file
240 '''
241 return self._pagecount
242
243 def get_total_height(self):
244 '''
245 Returns the total height of the Epub in pixels
246 '''
247 return self._bookheight
248
249
250 class _JobFind(GObject.GObject):
251 __gsignals__ = {
252 'updated': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ([])),
253 }
254
255 def __init__(self, document, start_page, n_pages, text,
256 case_sensitive=False):
257 """
258 Only case_sensitive=False is implemented
259 """
260 GObject.GObject.__init__(self)
261
262 self._finished = False
263 self._document = document
264 self._start_page = start_page
265 self._n_pages = n_pages
266 self._text = text
267 self._case_sensitive = case_sensitive
268 self.flattoc = self._document.get_flattoc()
269 self._matchfilelist = []
270 self._current_file_index = 0
271 self.threads = []
272
273 s_thread = SearchThread(self)
274 self.threads.append(s_thread)
275 s_thread.start()
276
277 def cancel(self):
278 '''
279 Cancels the search job
280 '''
281 for s_thread in self.threads:
282 s_thread.stop()
283
284 def is_finished(self):
285 '''
286 Returns True if the entire search job has been finished
287 '''
288 return self._finished
289
290 def get_next_file(self):
291 '''
292 Returns the next file which has the search pattern
293 '''
294 self._current_file_index += 1
295 try:
296 path = self._matchfilelist[self._current_file_index]
297 except IndexError:
298 self._current_file_index = 0
299 path = self._matchfilelist[self._current_file_index]
300
301 return path
302
303 def get_prev_file(self):
304 '''
305 Returns the previous file which has the search pattern
306 '''
307 self._current_file_index -= 1
308 try:
309 path = self._matchfilelist[self._current_file_index]
310 except IndexError:
311 self._current_file_index = -1
312 path = self._matchfilelist[self._current_file_index]
313
314 return path
315
316 def get_search_text(self):
317 '''
318 Returns the search text
319 '''
320 return self._text
321
322 def get_case_sensitive(self):
323 '''
324 Returns True if the search is case-sensitive
325 '''
326 return self._case_sensitive
+0
-102
epubview/navmap.py less more
0 import xml.etree.ElementTree as etree
1 from gi.repository import Gtk
2
3
4 class NavPoint(object):
5
6 def __init__(self, label, contentsrc, children=[]):
7 self._label = label
8 self._contentsrc = contentsrc
9 self._children = children
10
11 def get_label(self):
12 return self._label
13
14 def get_contentsrc(self):
15 return self._contentsrc
16
17 def get_children(self):
18 return self._children
19
20
21 class NavMap(object):
22 def __init__(self, opffile, ncxfile, basepath):
23 self._basepath = basepath
24 self._opffile = opffile
25 self._tree = etree.parse(ncxfile)
26 self._root = self._tree.getroot()
27 self._gtktreestore = Gtk.TreeStore(str, str)
28 self._flattoc = []
29
30 self._populate_flattoc()
31 self._populate_toc()
32
33 def _populate_flattoc(self):
34 tree = etree.parse(self._opffile)
35 root = tree.getroot()
36
37 itemmap = {}
38 manifest = root.find('.//{http://www.idpf.org/2007/opf}manifest')
39 for element in manifest.iterfind('{http://www.idpf.org/2007/opf}item'):
40 itemmap[element.get('id')] = element
41
42 spine = root.find('.//{http://www.idpf.org/2007/opf}spine')
43 for element in spine.iterfind('{http://www.idpf.org/2007/opf}itemref'):
44 idref = element.get('idref')
45 href = itemmap[idref].get('href')
46 self._flattoc.append(self._basepath + href)
47
48 self._opffile.close()
49
50 def _populate_toc(self):
51 navmap = self._root.find(
52 '{http://www.daisy.org/z3986/2005/ncx/}navMap')
53 for navpoint in navmap.iterfind(
54 './{http://www.daisy.org/z3986/2005/ncx/}navPoint'):
55 self._process_navpoint(navpoint)
56
57 def _gettitle(self, navpoint):
58 text = navpoint.find(
59 './{http://www.daisy.org/z3986/2005/ncx/}' +
60 'navLabel/{http://www.daisy.org/z3986/2005/ncx/}text')
61 return text.text
62
63 def _getcontent(self, navpoint):
64 text = navpoint.find(
65 './{http://www.daisy.org/z3986/2005/ncx/}content')
66 if text is not None:
67 return self._basepath + text.get('src')
68 else:
69 return ""
70
71 def _process_navpoint(self, navpoint, parent=None):
72 title = self._gettitle(navpoint)
73 content = self._getcontent(navpoint)
74
75 # print title, content
76
77 iter = self._gtktreestore.append(parent, [title, content])
78 # self._flattoc.append((title, content))
79
80 childnavpointlist = list(navpoint.iterfind(
81 './{http://www.daisy.org/z3986/2005/ncx/}navPoint'))
82
83 if len(childnavpointlist):
84 for childnavpoint in childnavpointlist:
85 self._process_navpoint(childnavpoint, parent=iter)
86 else:
87 return
88
89 def get_gtktreestore(self):
90 '''
91 Returns a GtkTreeModel representation of the
92 Epub table of contents
93 '''
94 return self._gtktreestore
95
96 def get_flattoc(self):
97 '''
98 Returns a flat (linear) list of files to be
99 rendered.
100 '''
101 return self._flattoc
+0
-117
epubview/widgets.py less more
0 import logging
1
2 import gi
3 gi.require_version('WebKit', '3.0')
4
5 from gi.repository import WebKit
6 from gi.repository import Gdk
7 from gi.repository import GObject
8
9
10 class _WebView(WebKit.WebView):
11
12 __gsignals__ = {
13 'touch-change-page': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE,
14 ([bool])), }
15
16 def __init__(self, only_to_measure=False):
17 WebKit.WebView.__init__(self)
18 self._only_to_measure = only_to_measure
19
20 def setup_touch(self):
21 self.get_window().set_events(
22 self.get_window().get_events() | Gdk.EventMask.TOUCH_MASK)
23 self.connect('event', self.__event_cb)
24
25 def __event_cb(self, widget, event):
26 if event.type == Gdk.EventType.TOUCH_BEGIN:
27 x = event.touch.x
28 view_width = widget.get_allocation().width
29 if x > view_width * 3 / 4:
30 self.emit('touch-change-page', True)
31 elif x < view_width * 1 / 4:
32 self.emit('touch-change-page', False)
33
34 def get_page_height(self):
35 '''
36 Gets height (in pixels) of loaded (X)HTML page.
37 This is done via javascript at the moment
38 '''
39 hide_scrollbar_js = ''
40 if self._only_to_measure:
41 hide_scrollbar_js = \
42 'document.documentElement.style.overflow = "hidden";'
43
44 oldtitle = self.get_main_frame().get_title()
45
46 js = """
47 document.documentElement.style.margin = "50px";
48 if (document.body == null) {
49 document.title = 0;
50 } else {
51 %s
52 document.title=Math.max(document.body.scrollHeight,
53 document.body.offsetHeight,
54 document.documentElement.clientHeight,
55 document.documentElement.scrollHeight,
56 document.documentElement.offsetHeight);
57 };
58 """ % hide_scrollbar_js
59 self.execute_script(js)
60 ret = self.get_main_frame().get_title()
61 self.execute_script('document.title=%s;' % oldtitle)
62 try:
63 return int(ret)
64 except ValueError:
65 return 0
66
67 def add_bottom_padding(self, incr):
68 '''
69 Adds incr pixels of padding to the end of the loaded (X)HTML page.
70 This is done via javascript at the moment
71 '''
72 js = """
73 var newdiv = document.createElement("div");
74 newdiv.style.height = "%dpx";
75 document.body.appendChild(newdiv);
76 """ % incr
77 self.execute_script(js)
78
79 def highlight_next_word(self):
80 '''
81 Highlight next word (for text to speech)
82 '''
83 self.execute_script('highLightNextWord();')
84
85 def go_to_link(self, id_link):
86 self.execute_script('window.location.href = "%s";' % id_link)
87
88 def get_vertical_position_element(self, id_link):
89 '''
90 Get the vertical position of a element, in pixels
91 '''
92 # remove the first '#' char
93 id_link = id_link[1:]
94 oldtitle = self.get_main_frame().get_title()
95 js = """
96 obj = document.getElementById('%s');
97 var top = 0;
98 if(obj.offsetParent) {
99 while(1) {
100 top += obj.offsetTop;
101 if(!obj.offsetParent) {
102 break;
103 };
104 obj = obj.offsetParent;
105 };
106 } else if(obj.y) {
107 top += obj.y;
108 };
109 document.title=top;""" % id_link
110 self.execute_script(js)
111 ret = self.get_main_frame().get_title()
112 self.execute_script('document.title=%s;' % oldtitle)
113 try:
114 return int(ret)
115 except ValueError:
116 return 0
821821 del self.unused_download_tubes
822822
823823 # Use the suggested file, the mime is not recognized if the extension
824 # is wrong in some cases (epub)
824 # is wrong in some cases
825825 temp_dir = os.path.dirname(tempfile)
826826 new_name = os.path.join(temp_dir, suggested_name)
827827 os.rename(tempfile, new_name)
975975 else:
976976 mimetype = self.metadata['mime_type']
977977
978 if mimetype == 'application/epub+zip':
979 import epubadapter
980 self._view = epubadapter.EpubViewer()
981 elif mimetype == 'text/plain' or mimetype == 'application/zip':
978 if mimetype == 'text/plain' or mimetype == 'application/zip':
982979 import textadapter
983980 self._view = textadapter.TextViewer()
984981 elif mimetype == 'application/x-cbz':