diff --git a/setup.py b/setup.py index a1061478ce..948db8039c 100644 --- a/setup.py +++ b/setup.py @@ -145,6 +145,7 @@ if __name__ == '__main__': fb2_xsl = 'ebooks/lrf/fb2/fb2.xsl', metadata_sqlite = 'library/metadata_sqlite.sql', jquery = 'gui2/viewer/jquery.js', + jquery_scrollTo = 'gui2/viewer/jquery_scrollTo.js', ) DEST = os.path.join('src', APPNAME, 'resources.py') @@ -291,7 +292,7 @@ if __name__ == '__main__': if form.endswith('viewer%smain.ui'%os.sep): print 'Promoting WebView' - dat = dat.replace('self.view = QtWebKit.QWebView(self.widget)', 'self.view = DocumentView(self.widget)') + dat = dat.replace('self.view = QtWebKit.QWebView(', 'self.view = DocumentView(') dat += '\n\nfrom calibre.gui2.viewer.documentview import DocumentView' open(compiled_form, 'wb').write(dat) diff --git a/src/calibre/ebooks/epub/iterator.py b/src/calibre/ebooks/epub/iterator.py index e41aa47141..4dd7fe1cc8 100644 --- a/src/calibre/ebooks/epub/iterator.py +++ b/src/calibre/ebooks/epub/iterator.py @@ -6,6 +6,7 @@ Iterate over the HTML files in an ebook. Useful for writing viewers. ''' import re, os, math, copy +from cStringIO import StringIO from PyQt4.Qt import QFontDatabase @@ -16,6 +17,8 @@ from calibre.ebooks.metadata.opf2 import OPF from calibre.ptempfile import TemporaryDirectory from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.html import create_dir +from calibre.utils.zipfile import safe_replace, ZipFile +from calibre.utils.config import DynamicConfig def character_count(html): ''' @@ -62,7 +65,8 @@ class EbookIterator(object): CHARACTERS_PER_PAGE = 1000 def __init__(self, pathtoebook): - self.pathtoebook = pathtoebook + self.pathtoebook = os.path.abspath(pathtoebook) + self.config = DynamicConfig(name='iterator') ext = os.path.splitext(pathtoebook)[1].replace('.', '').lower() ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext) map = dict(MAP) @@ -80,6 +84,9 @@ class EbookIterator(object): return i def find_embedded_fonts(self): + ''' + This will become unnecessary once Qt WebKit supports the @font-face rule. + ''' for item in self.opf.manifest: if item.mime_type and 'css' in item.mime_type.lower(): css = open(item.path, 'rb').read().decode('utf-8') @@ -125,9 +132,60 @@ class EbookIterator(object): s.max_page = s.start_page + s.pages - 1 self.toc = self.opf.toc - self.find_embedded_fonts() + self.find_embedded_fonts() + self.read_bookmarks() return self + + def parse_bookmarks(self, raw): + for line in raw.splitlines(): + if line.count('^') > 0: + tokens = line.rpartition('^') + title, ref = tokens[0], tokens[2] + self.bookmarks.append((title, ref)) + + def serialize_bookmarks(self, bookmarks): + dat = [] + for title, bm in bookmarks: + dat.append(u'%s^%s'%(title, bm)) + return (u'\n'.join(dat) +'\n').encode('utf-8') + + def read_bookmarks(self): + self.bookmarks = [] + bmfile = os.path.join(self.base, 'META-INF', 'calibre_bookmarks.txt') + raw = '' + if os.path.exists(bmfile): + raw = open(bmfile, 'rb').read().decode('utf-8') + else: + saved = self.config['bookmarks_'+self.pathtoebook] + if saved: + raw = saved + self.parse_bookmarks(raw) + + def save_bookmarks(self, bookmarks=None): + if bookmarks is None: + bookmarks = self.bookmarks + dat = self.serialize_bookmarks(bookmarks) + if os.path.splitext(self.pathtoebook)[1].lower() == '.epub': + zf = open(self.pathtoebook, 'r+b') + zipf = ZipFile(zf, mode='a') + for name in zipf.namelist(): + if name == 'META-INF/calibre_bookmarks.txt': + safe_replace(zf, 'META-INF/calibre_bookmarks.txt', StringIO(dat)) + return + zipf.writestr('META-INF/calibre_bookmarks.txt', dat) + else: + self.config['bookmarks_'+self.pathtoebook] = dat + + def add_bookmark(self, bm): + dups = [] + for x in self.bookmarks: + if x[0] == bm[0]: + dups.append(x) + for x in dups: + self.bookmarks.remove(x) + self.bookmarks.append(bm) + self.save_bookmarks() def __exit__(self, *args): self._tdir.__exit__(*args) \ No newline at end of file diff --git a/src/calibre/ebooks/html.py b/src/calibre/ebooks/html.py index e10aa9a5b3..a1ec03a16e 100644 --- a/src/calibre/ebooks/html.py +++ b/src/calibre/ebooks/html.py @@ -429,7 +429,7 @@ class Parser(PreProcessor, LoggingInterface): if not m: return ''%raw else: - return match.group().sub(m.group('uri'), "http://www.w3.org/1999/xhtml") + return match.group().replace(m.group('uri'), "http://www.w3.org/1999/xhtml") def save(self): ''' diff --git a/src/calibre/ebooks/lrf/html/convert_from.py b/src/calibre/ebooks/lrf/html/convert_from.py index 76a6825633..f219b92c67 100644 --- a/src/calibre/ebooks/lrf/html/convert_from.py +++ b/src/calibre/ebooks/lrf/html/convert_from.py @@ -809,8 +809,9 @@ class HTMLConverter(object, LoggingInterface): def append_text(src): fp, key, variant = self.font_properties(css) - src = src.replace(u'\xa0', ' ') #Sony's wonderful reading software doesn't handle the nbsp character - + for x, y in [(u'\xa0', ' '), (u'\ufb00', 'ff'), (u'\ufb01', 'fi'), (u'\ufb02', 'fl'), (u'\ufb03', 'ffi'), (u'\ufb04', 'ffl')]: + src = src.replace(x, y) + valigner = lambda x: x if 'vertical-align' in css: valign = css['vertical-align'] diff --git a/src/calibre/gui2/images/bookmarks.svg b/src/calibre/gui2/images/bookmarks.svg new file mode 100644 index 0000000000..2fcd844283 --- /dev/null +++ b/src/calibre/gui2/images/bookmarks.svg @@ -0,0 +1,590 @@ + + + diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 949c5c116a..82cb9e7002 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -904,11 +904,7 @@ class Main(MainWindow, Ui_MainWindow): #############################View book###################################### def view_format(self, row, format): - pt = PersistentTemporaryFile('_viewer.'+format.lower()) - pt.write(self.library_view.model().db.format(row, format)) - pt.close() - self.persistent_files.append(pt) - self._view_file(pt.name) + self._view_file(self.library_view.model().db.format(row, format, as_file=True).name) def book_downloaded_for_viewing(self, job): if job.exception: diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 97aa37052f..b0e85ba181 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -13,7 +13,7 @@ from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from calibre.utils.config import Config, StringConfig from calibre.gui2.viewer.config_ui import Ui_Dialog - +from calibre.gui2.viewer.js import bookmarks, referencing def load_builtin_fonts(): from calibre.ebooks.lrf.fonts.liberation import LiberationMono_BoldItalic @@ -134,8 +134,23 @@ class Document(QWebPage): self.load_javascript_libraries) def load_javascript_libraries(self): - from calibre.resources import jquery + from calibre.resources import jquery, jquery_scrollTo self.javascript(jquery) + self.javascript(jquery_scrollTo) + self.javascript(bookmarks) + self.javascript(referencing) + + def reference_mode(self, enable): + self.javascript(('enter' if enable else 'leave')+'_reference_mode()') + + def set_reference_prefix(self, prefix): + self.javascript('reference_prefix = "%s"'%prefix) + + def goto(self, ref): + self.javascript('goto_reference("%s")'%ref) + + def goto_bookmark(self, bm): + self.javascript('scroll_to_bookmark("%s")'%bm) def javascript(self, string, typ=None): ans = self.mainFrame().evaluateJavaScript(string) @@ -162,7 +177,10 @@ class Document(QWebPage): r = self.height%self.window_height if r > 0: self.javascript('document.body.style.paddingBottom = "%dpx"'%r) - + + def bookmark(self): + return self.javascript('calculate_bookmark(%d)'%(self.ypos+25), 'string') + @apply def at_bottom(): def fget(self): @@ -190,6 +208,12 @@ class Document(QWebPage): def fget(self): return self.javascript('window.innerHeight', 'int') return property(fget=fget) + + @apply + def window_width(): + def fget(self): + return self.javascript('window.innerWidth', 'int') + return property(fget=fget) @apply def xpos(): @@ -249,15 +273,29 @@ class DocumentView(QWebView): self.document = Document(self) self.setPage(self.document) self.manager = None + self._reference_mode = False self.connect(self.document, SIGNAL('loadStarted()'), self.load_started) self.connect(self.document, SIGNAL('loadFinished(bool)'), self.load_finished) self.connect(self.document, SIGNAL('linkClicked(QUrl)'), self.link_clicked) self.connect(self.document, SIGNAL('linkHovered(QString,QString,QString)'), self.link_hovered) self.connect(self.document, SIGNAL('selectionChanged()'), self.selection_changed) + def reference_mode(self, enable): + self._reference_mode = enable + self.document.reference_mode(enable) + + def goto(self, ref): + self.document.goto(ref) + + def goto_bookmark(self, bm): + self.document.goto_bookmark(bm) + def config(self, parent=None): self.document.do_config(parent) + def bookmark(self): + return self.document.bookmark() + def selection_changed(self): if self.manager is not None: self.manager.selection_changed(unicode(self.document.selectedText())) @@ -344,8 +382,11 @@ class DocumentView(QWebView): self.initial_pos = 0.0 self.update() self.initialize_scrollbar() + self.document.reference_mode(self._reference_mode) if self.manager is not None: - self.manager.load_finished(bool(ok)) + spine_index = self.manager.load_finished(bool(ok)) + if spine_index > -1: + self.document.set_reference_prefix('%d.'%(spine_index+1)) if scrolled: self.manager.scrolled(self.document.scroll_fraction) diff --git a/src/calibre/gui2/viewer/jquery_scrollTo.js b/src/calibre/gui2/viewer/jquery_scrollTo.js new file mode 100644 index 0000000000..7f4248922f --- /dev/null +++ b/src/calibre/gui2/viewer/jquery_scrollTo.js @@ -0,0 +1,194 @@ +/** + * jQuery.ScrollTo + * Copyright (c) 2007-2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com + * Dual licensed under MIT and GPL. + * Date: 9/11/2008 + * + * @projectDescription Easy element scrolling using jQuery. + * http://flesler.blogspot.com/2007/10/jqueryscrollto.html + * Tested with jQuery 1.2.6. On FF 2/3, IE 6/7, Opera 9.2/5 and Safari 3. on Windows. + * + * @author Ariel Flesler + * @version 1.4 + * + * @id jQuery.scrollTo + * @id jQuery.fn.scrollTo + * @param {String, Number, DOMElement, jQuery, Object} target Where to scroll the matched elements. + * The different options for target are: + * - A number position (will be applied to all axes). + * - A string position ('44', '100px', '+=90', etc ) will be applied to all axes + * - A jQuery/DOM element ( logically, child of the element to scroll ) + * - A string selector, that will be relative to the element to scroll ( 'li:eq(2)', etc ) + * - A hash { top:x, left:y }, x and y can be any kind of number/string like above. + * @param {Number} duration The OVERALL length of the animation, this argument can be the settings object instead. + * @param {Object,Function} settings Optional set of settings or the onAfter callback. + * @option {String} axis Which axis must be scrolled, use 'x', 'y', 'xy' or 'yx'. + * @option {Number} duration The OVERALL length of the animation. + * @option {String} easing The easing method for the animation. + * @option {Boolean} margin If true, the margin of the target element will be deducted from the final position. + * @option {Object, Number} offset Add/deduct from the end position. One number for both axes or { top:x, left:y }. + * @option {Object, Number} over Add/deduct the height/width multiplied by 'over', can be { top:x, left:y } when using both axes. + * @option {Boolean} queue If true, and both axis are given, the 2nd axis will only be animated after the first one ends. + * @option {Function} onAfter Function to be called after the scrolling ends. + * @option {Function} onAfterFirst If queuing is activated, this function will be called after the first scrolling ends. + * @return {jQuery} Returns the same jQuery object, for chaining. + * + * @desc Scroll to a fixed position + * @example $('div').scrollTo( 340 ); + * + * @desc Scroll relatively to the actual position + * @example $('div').scrollTo( '+=340px', { axis:'y' } ); + * + * @dec Scroll using a selector (relative to the scrolled element) + * @example $('div').scrollTo( 'p.paragraph:eq(2)', 500, { easing:'swing', queue:true, axis:'xy' } ); + * + * @ Scroll to a DOM element (same for jQuery object) + * @example var second_child = document.getElementById('container').firstChild.nextSibling; + * $('#container').scrollTo( second_child, { duration:500, axis:'x', onAfter:function(){ + * alert('scrolled!!'); + * }}); + * + * @desc Scroll on both axes, to different values + * @example $('div').scrollTo( { top: 300, left:'+=200' }, { axis:'xy', offset:-20 } ); + */ +;(function( $ ){ + + var $scrollTo = $.scrollTo = function( target, duration, settings ){ + $(window).scrollTo( target, duration, settings ); + }; + + $scrollTo.defaults = { + axis:'y', + duration:1 + }; + + // Returns the element that needs to be animated to scroll the window. + // Kept for backwards compatibility (specially for localScroll & serialScroll) + $scrollTo.window = function( scope ){ + return $(window).scrollable(); + }; + + // Hack, hack, hack... stay away! + // Returns the real elements to scroll (supports window/iframes, documents and regular nodes) + $.fn.scrollable = function(){ + return this.map(function(){ + // Just store it, we might need it + var win = this.parentWindow || this.defaultView, + // If it's a document, get its iframe or the window if it's THE document + elem = this.nodeName == '#document' ? win.frameElement || win : this, + // Get the corresponding document + doc = elem.contentDocument || (elem.contentWindow || elem).document, + isWin = elem.setInterval; + + return elem.nodeName == 'IFRAME' || isWin && $.browser.safari ? doc.body + : isWin ? doc.documentElement + : this; + }); + }; + + $.fn.scrollTo = function( target, duration, settings ){ + if( typeof duration == 'object' ){ + settings = duration; + duration = 0; + } + if( typeof settings == 'function' ) + settings = { onAfter:settings }; + + settings = $.extend( {}, $scrollTo.defaults, settings ); + // Speed is still recognized for backwards compatibility + duration = duration || settings.speed || settings.duration; + // Make sure the settings are given right + settings.queue = settings.queue && settings.axis.length > 1; + + if( settings.queue ) + // Let's keep the overall duration + duration /= 2; + settings.offset = both( settings.offset ); + settings.over = both( settings.over ); + + return this.scrollable().each(function(){ + var elem = this, + $elem = $(elem), + targ = target, toff, attr = {}, + win = $elem.is('html,body'); + + switch( typeof targ ){ + // A number will pass the regex + case 'number': + case 'string': + if( /^([+-]=)?\d+(px)?$/.test(targ) ){ + targ = both( targ ); + // We are done + break; + } + // Relative selector, no break! + targ = $(targ,this); + case 'object': + // DOMElement / jQuery + if( targ.is || targ.style ) + // Get the real position of the target + toff = (targ = $(targ)).offset(); + } + $.each( settings.axis.split(''), function( i, axis ){ + var Pos = axis == 'x' ? 'Left' : 'Top', + pos = Pos.toLowerCase(), + key = 'scroll' + Pos, + old = elem[key], + Dim = axis == 'x' ? 'Width' : 'Height', + dim = Dim.toLowerCase(); + + if( toff ){// jQuery / DOMElement + attr[key] = toff[pos] + ( win ? 0 : old - $elem.offset()[pos] ); + + // If it's a dom element, reduce the margin + if( settings.margin ){ + attr[key] -= parseInt(targ.css('margin'+Pos)) || 0; + attr[key] -= parseInt(targ.css('border'+Pos+'Width')) || 0; + } + + attr[key] += settings.offset[pos] || 0; + + if( settings.over[pos] ) + // Scroll to a fraction of its width/height + attr[key] += targ[dim]() * settings.over[pos]; + }else + attr[key] = targ[pos]; + + // Number or 'number' + if( /^\d+$/.test(attr[key]) ) + // Check the limits + attr[key] = attr[key] <= 0 ? 0 : Math.min( attr[key], max(Dim) ); + + // Queueing axes + if( !i && settings.queue ){ + // Don't waste time animating, if there's no need. + if( old != attr[key] ) + // Intermediate animation + animate( settings.onAfterFirst ); + // Don't animate this axis again in the next iteration. + delete attr[key]; + } + }); + animate( settings.onAfter ); + + function animate( callback ){ + $elem.animate( attr, duration, settings.easing, callback && function(){ + callback.call(this, target, settings); + }); + }; + function max( Dim ){ + var attr ='scroll'+Dim, + doc = elem.ownerDocument; + + return win + ? Math.max( doc.documentElement[attr], doc.body[attr] ) + : elem[attr]; + }; + }).end(); + }; + + function both( val ){ + return typeof val == 'object' ? val : { top:val, left:val }; + }; + +})( jQuery ); \ No newline at end of file diff --git a/src/calibre/gui2/viewer/js.py b/src/calibre/gui2/viewer/js.py new file mode 100644 index 0000000000..af5a2f5e80 --- /dev/null +++ b/src/calibre/gui2/viewer/js.py @@ -0,0 +1,121 @@ +bookmarks = ''' + +function find_enclosing_block(y) { + var elements = $("*", document.body); + var min = 0; + var temp, left, top, elem, width, height, ratio; + for (i = 0; i < elements.length; i++) { + elem = $(elements[i]); + temp = elem.offset(); + left = temp.left; top = temp.top; + width = elem.width(); height = elem.height(); + if (top > y+50) break; + for ( x = 40; x < window.innerWidth; x += 20) { + if (x >= left && x <= left+width && y >= top && y <= top+height) { + if (min == 0 || min.height() > height) { min = elem; break; } + } + } + if (min != 0 && min.height() < 200) break; + } + if (y <= 0) return document.body; + if (min == 0) { return find_enclosing_block(x, y-20); } + return min; +} + +function selector_in_parent(elem) { + var num = elem.prevAll().length; + var sel = " > *:eq("+num+") "; + return sel; +} + +function selector(elem) { + var obj = elem; + var sel = ""; + while (obj[0] != document) { + sel = selector_in_parent(obj) + sel; + obj = obj.parent(); + } + return sel; +} + +function calculate_bookmark(y) { + var elem = find_enclosing_block(y); + var sel = selector(elem); + var ratio = (y - elem.offset().top)/elem.height(); + if (ratio > 1) { ratio = 1; } + if (ratio < 0) { ratio = 0; } + return sel + "|" + ratio; +} + +function scroll_to_bookmark(bookmark) { + bm = bookmark.split("|"); + var ratio = 0.7 * parseFloat(bm[1]); + $.scrollTo($(bm[0]), 1000, {over:ratio}); +} + +''' + +referencing = ''' +var reference_old_bgcol = "transparent"; +var reference_prefix = "1."; + +function show_reference_panel(ref) { + panel = $("#calibre_reference_panel"); + if (panel.length < 1) { + $(document.body).append('
None