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
3 | 3 | icon = activity-read |
4 | 4 | exec = sugar-activity readactivity.ReadActivity |
5 | 5 | 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 | |
7 | 7 | license = GPLv2+ |
8 | 8 | 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! |
9 | 9 | categories = language documents media system |
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 | 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 | # 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 | # 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 | 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 | # 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 | 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 | # 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 | 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 | 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 |
821 | 821 | del self.unused_download_tubes |
822 | 822 | |
823 | 823 | # 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 | |
825 | 825 | temp_dir = os.path.dirname(tempfile) |
826 | 826 | new_name = os.path.join(temp_dir, suggested_name) |
827 | 827 | os.rename(tempfile, new_name) |
975 | 975 | else: |
976 | 976 | mimetype = self.metadata['mime_type'] |
977 | 977 | |
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': | |
982 | 979 | import textadapter |
983 | 980 | self._view = textadapter.TextViewer() |
984 | 981 | elif mimetype == 'application/x-cbz': |