Remove old viewer and coffeescript and viewer resources

Since the PDF output code is also going to be replaced, none
of this is required.
This commit is contained in:
Kovid Goyal 2019-06-27 08:30:23 +05:30
parent 2224f8e7ae
commit 070ad5351e
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
52 changed files with 7 additions and 20111 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>blank</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
</head>
<body>
<div>&nbsp;</div>
</body>
</html>

View File

@ -1,49 +0,0 @@
/*
* bookmarks management
* Copyright 2008 Kovid Goyal
* License: GNU GPL v3
*/
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();
}
if (sel.length > 2 && sel.charAt(1) == ">") sel = sel.substring(2);
return sel;
}
function calculate_bookmark(y, node) {
var elem = $(node);
var sel = selector(elem);
var ratio = (y - elem.offset().top)/elem.height();
if (ratio > 1) { ratio = 1; }
if (ratio < 0) { ratio = 0; }
sel = sel + "|" + ratio;
return sel;
}
function animated_scrolling_done() {
window.py_bridge.animated_scroll_done();
}
function scroll_to_bookmark(bookmark) {
bm = bookmark.split("|");
var ratio = 0.7 * parseFloat(bm[1]);
$.scrollTo($(bm[0]), 1000,
{
over:ratio,
axis: 'y', // Do not scroll in the x direction
onAfter:function(){window.py_bridge.animated_scroll_done()}
}
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +0,0 @@
/*
* Hyphenation
* Copyright 2008 Kovid Goyal
* License: GNU GPL v3
*/
function do_hyphenation(lang) {
Hyphenator.config(
{
'minwordlength' : 6,
// 'hyphenchar' : '|',
'displaytogglebox' : false,
'remoteloading' : false,
'doframes' : true,
'defaultlanguage' : 'en',
'storagetype' : 'session',
'onerrorhandler' : function (e) {
window.py_bridge.debug(e);
}
});
// console.log(lang);
Hyphenator.hyphenate(document.body, lang);
}
function hyphenate_text(text, lang) {
return Hyphenator.hyphenate(text, lang);
}

View File

@ -1,52 +0,0 @@
/*
* images management
* Copyright 2008 Kovid Goyal
* License: GNU GPL v3
*/
function scale_images() {
$("img:visible").each(function() {
var img = $(this);
var offset = img.offset();
var avail_width = window.innerWidth - offset.left - 5;
var avail_height = window.innerHeight - 5;
img.css('width', img.data('orig-width'));
img.css('height', img.data('orig-height'));
var width = img.width();
var height = img.height();
var ratio = 0;
if (width > avail_width) {
ratio = avail_width / width;
img.css('width', avail_width+'px');
img.css('height', (ratio*height) + 'px');
height = height * ratio;
width = width * ratio;
}
if (height > avail_height) {
ratio = avail_height / height;
img.css('height', avail_height);
img.css('width', width * ratio);
}
//window.py_bridge.debug(window.getComputedStyle(this, '').getPropertyValue('max-width'));
});
}
function store_original_size_attributes() {
$("img").each(function() {
var img = $(this);
img.data('orig-width', img.css('width'));
img.data('orig-height', img.css('height'));
});
}
function setup_image_scaling_handlers() {
store_original_size_attributes();
scale_images();
$(window).resize(function(){
scale_images();
});
}

File diff suppressed because it is too large Load Diff

View File

@ -1,215 +0,0 @@
/**
* jQuery.ScrollTo
* Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
* Dual licensed under MIT and GPL.
* Date: 5/25/2009
*
* @projectDescription Easy element scrolling using jQuery.
* http://flesler.blogspot.com/2007/10/jqueryscrollto.html
* Works with jQuery +1.2.6. Tested on FF 2/3, IE 6/7/8, Opera 9.5/6, Safari 3, Chrome 1 on WinXP.
*
* @author Ariel Flesler
* @version 1.4.2
*
* @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.
* - A percentage of the container's dimension/s, for example: 50% to go to the middle.
* - The string 'max' for go-to-end.
* @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:'xy',
duration: parseFloat($.fn.jquery) >= 1.3 ? 0 : 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 :)
// Returns the real elements to scroll (supports window/iframes, documents and regular nodes)
$.fn._scrollable = function(){
return this.map(function(){
var elem = this,
isWin = !elem.nodeName || $.inArray( elem.nodeName.toLowerCase(), ['iframe','#document','html','body'] ) != -1;
if( !isWin )
return elem;
var doc = (elem.contentWindow || elem).document || elem.ownerDocument || elem;
return $.browser.safari || doc.compatMode == 'BackCompat' ?
doc.body :
doc.documentElement;
});
};
$.fn.scrollTo = function( target, duration, settings ){
if( typeof duration == 'object' ){
settings = duration;
duration = 0;
}
if( typeof settings == 'function' )
settings = { onAfter:settings };
if( target == 'max' )
target = 9e9;
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+(\.\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],
max = $scrollTo.max(elem, axis);
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[axis=='x'?'width':'height']() * settings.over[pos];
}else{
var val = targ[pos];
// Handle percentage values
attr[key] = val.slice && val.slice(-1) == '%' ?
parseFloat(val) / 100 * max
: val;
}
// Number or 'number'
if( /^\d+$/.test(attr[key]) )
// Check the limits
attr[key] = attr[key] <= 0 ? 0 : Math.min( attr[key], max );
// 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);
});
};
}).end();
};
// Max scrolling position, works on quirks mode
// It only fails (not too badly) on IE, quirks mode.
$scrollTo.max = function( elem, axis ){
var Dim = axis == 'x' ? 'Width' : 'Height',
scroll = 'scroll'+Dim;
if( !$(elem).is('html,body') )
return elem[scroll] - $(elem)[Dim.toLowerCase()]();
var size = 'client' + Dim,
html = elem.ownerDocument.documentElement,
body = elem.ownerDocument.body;
return Math.max( html[scroll], body[scroll] )
- Math.min( html[size] , body[size] );
};
function both( val ){
return typeof val == 'object' ? val : { top:val, left:val };
};
})( jQuery );

View File

@ -1,72 +0,0 @@
/*
* reference management
* Copyright 2008 Kovid Goyal
* License: GNU GPL v3
*/
var reference_old_bgcol = "transparent";
var reference_prefix = "1.";
var reference_last_highlighted_para = null;
function show_reference_panel(ref) {
panel = $("#calibre_reference_panel");
if (panel.length < 1) {
$(document.body).append('<div id="calibre_reference_panel" style="top:20px; left:20px; padding-left:30px; padding-right:30px; font:monospace normal;text-align:center; z-index:10000; background: beige; border:red ridge 2px; position:absolute; color: black"><h5>Paragraph</h5><p style="text-indent:0pt">None</p></div>');
panel = $("#calibre_reference_panel");
}
$("> p", panel).text(ref);
panel.css({top:(window.pageYOffset+20)+"px", left:(window.pageXOffset+20)+"px"});
panel.fadeIn(500);
}
function toggle_reference(e) {
p = $(this);
if (e.type == "mouseenter") {
reference_old_bgcol = p.css("background-color");
reference_last_highlighted_para = p;
p.css({backgroundColor:"beige"});
var i = 0;
var paras = $("p");
for (j = 0; j < paras.length; j++,i++) {
if (paras[j] == p[0]) break;
}
show_reference_panel(reference_prefix+(i+1) );
} else {
p.css({backgroundColor:reference_old_bgcol});
panel = $("#calibre_reference_panel").hide();
reference_last_highlighted_para = null;
}
return false;
}
function enter_reference_mode() {
$("p").bind("mouseenter mouseleave", toggle_reference);
}
function leave_reference_mode() {
$("p").unbind("mouseenter mouseleave", toggle_reference);
panel = $("#calibre_reference_panel");
if (panel.length > 0) panel.hide();
if (reference_last_highlighted_para !== null)
reference_last_highlighted_para.css({backgroundColor:reference_old_bgcol});
}
function goto_reference(ref) {
var tokens = ref.split(".");
if (tokens.length != 2) {alert("Invalid reference: "+ref); return;}
var num = parseInt(tokens[1]);
if (isNaN(num)) {alert("Invalid reference: "+ref); return;}
num -= 1;
if (num < 0) {alert("Invalid reference: "+ref); return;}
var p = $("p");
if (num >= p.length) {alert("Reference not found: "+ref); return;}
var dest = $(p[num]);
if (window.paged_display.in_paged_mode) {
var xpos = dest.offset().left;
window.paged_display.scroll_to_xpos(xpos, true, true, 1000);
} else
$.scrollTo(dest, 1000,
{onAfter:function(){window.py_bridge.animated_scroll_done();}});
}

View File

@ -37,8 +37,6 @@ class Check(Command):
'unicodepoints.py', 'krcodepoints.py', 'jacodepoints.py', 'vncodepoints.py', 'zhcodepoints.py') and
'prs500/driver.py' not in y) and not f.endswith('_ui.py'):
yield y
if f.endswith('.coffee'):
yield y
for x in os.walk(self.j(self.d(self.SRC), 'recipes')):
for f in x[-1]:
@ -90,12 +88,6 @@ class Check(Command):
import whats_new
whats_new.render_changelog(self.j(self.d(self.SRC), 'Changelog.yaml'))
sys.path.remove(self.wn_path)
else:
from calibre.utils.serve_coffee import check_coffeescript
try:
check_coffeescript(f)
except:
return True
def run(self, opts):
self.fhash_cache = {}

View File

@ -12,7 +12,7 @@ __all__ = [
'gui',
'git_version',
'develop', 'install',
'kakasi', 'coffee', 'rapydscript', 'cacerts', 'recent_uas', 'resources',
'kakasi', 'rapydscript', 'cacerts', 'recent_uas', 'resources',
'check', 'to3', 'unicode_check', 'iterators_check', 'test',
'sdist', 'bootstrap',
'manual', 'tag_release',
@ -63,10 +63,9 @@ iterators_check = IteratorsCheck()
from setup.test import Test
test = Test()
from setup.resources import Resources, Kakasi, Coffee, CACerts, RapydScript, RecentUAs
from setup.resources import Resources, Kakasi, CACerts, RapydScript, RecentUAs
resources = Resources()
kakasi = Kakasi()
coffee = Coffee()
cacerts = CACerts()
recent_uas = RecentUAs()
rapydscript = RapydScript()

View File

@ -6,9 +6,8 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, re, shutil, zipfile, glob, time, sys, hashlib, json, errno
import os, re, shutil, zipfile, glob, json, errno
from zlib import compress
from itertools import chain
is_ci = os.environ.get('CI', '').lower() == 'true'
from setup import Command, basenames, __appname__, download_securely, dump_json
@ -30,102 +29,6 @@ def get_opts_from_parser(parser):
yield x
class Coffee(Command): # {{{
description = 'Compile coffeescript files into javascript'
COFFEE_DIRS = ('ebooks/oeb/display', 'ebooks/oeb/polish')
def add_options(self, parser):
parser.add_option('--watch', '-w', action='store_true', default=False,
help='Autocompile when .coffee files are changed')
parser.add_option('--show-js', action='store_true', default=False,
help='Display the generated javascript')
def run(self, opts):
self.do_coffee_compile(opts)
if opts.watch:
try:
while True:
time.sleep(0.5)
self.do_coffee_compile(opts, timestamp=True,
ignore_errors=True)
except KeyboardInterrupt:
pass
def show_js(self, raw):
from pygments.lexers import JavascriptLexer
from pygments.formatters import TerminalFormatter
from pygments import highlight
print(highlight(raw, JavascriptLexer(), TerminalFormatter()))
def do_coffee_compile(self, opts, timestamp=False, ignore_errors=False):
from calibre.utils.serve_coffee import compile_coffeescript
src_files = {}
for src in self.COFFEE_DIRS:
for f in glob.glob(self.j(self.SRC, __appname__, src,
'*.coffee')):
bn = self.b(f).rpartition('.')[0]
arcname = src.replace('/', '.') + '.' + bn + '.js'
try:
with open(f, 'rb') as fs:
src_files[arcname] = (f, hashlib.sha1(fs.read()).hexdigest())
except EnvironmentError:
time.sleep(0.1)
with open(f, 'rb') as fs:
src_files[arcname] = (f, hashlib.sha1(fs.read()).hexdigest())
existing = {}
dest = self.j(self.RESOURCES, 'compiled_coffeescript.zip')
if os.path.exists(dest):
with zipfile.ZipFile(dest, 'r') as zf:
existing_hashes = {}
raw = zf.comment
if raw:
existing_hashes = json.loads(raw)
for info in zf.infolist():
if info.filename in existing_hashes and src_files.get(info.filename, (None, None))[1] == existing_hashes[info.filename]:
existing[info.filename] = (zf.read(info), info, existing_hashes[info.filename])
todo = set(src_files) - set(existing)
updated = {}
for arcname in todo:
name = arcname.rpartition('.')[0]
print('\t%sCompiling %s'%(time.strftime('[%H:%M:%S] ') if
timestamp else '', name))
src, sig = src_files[arcname]
js, errors = compile_coffeescript(open(src, 'rb').read(), filename=src)
if errors:
print('\n\tCompilation of %s failed'%name)
for line in errors:
print(line, file=sys.stderr)
if ignore_errors:
js = u'# Compilation from coffeescript failed'
else:
raise SystemExit(1)
else:
if opts.show_js:
self.show_js(js)
print('#'*80)
print('#'*80)
zi = zipfile.ZipInfo()
zi.filename = arcname
zi.date_time = time.localtime()[:6]
updated[arcname] = (js.encode('utf-8'), zi, sig)
if updated:
hashes = {}
with zipfile.ZipFile(dest, 'w', zipfile.ZIP_STORED) as zf:
for raw, zi, sig in sorted(chain(itervalues(updated), itervalues(existing)), key=lambda x: x[1].filename):
zf.writestr(zi, raw)
hashes[zi.filename] = sig
zf.comment = json.dumps(hashes)
def clean(self):
x = self.j(self.RESOURCES, 'compiled_coffeescript.zip')
if os.path.exists(x):
os.remove(x)
# }}}
class Kakasi(Command): # {{{
description = 'Compile resources for unihandecode'
@ -296,7 +199,7 @@ class RapydScript(Command): # {{{
class Resources(Command): # {{{
description = 'Compile various needed calibre resources'
sub_commands = ['kakasi', 'coffee', 'mathjax', 'rapydscript']
sub_commands = ['kakasi', 'mathjax', 'rapydscript']
def run(self, opts):
from calibre.utils.serialize import msgpack_dumps
@ -412,9 +315,8 @@ class Resources(Command): # {{{
x = self.j(self.RESOURCES, x+'.pickle')
if os.path.exists(x):
os.remove(x)
from setup.commands import kakasi, coffee
from setup.commands import kakasi
kakasi.clean()
coffee.clean()
for x in ('builtin_recipes.xml', 'builtin_recipes.zip',
'template-functions.json', 'user-manual-translation-stats.json'):
x = self.j(self.RESOURCES, x)

View File

@ -1,671 +0,0 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2011, Kovid Goyal <kovid@kovidgoyal.net>
Released under the GPLv3 License
Based on code originally written by Peter Sorotkin
http://code.google.com/p/epub-revision/source/browse/trunk/src/samples/cfi/epubcfi.js
Improvements with respect to that code:
1. Works on all browsers (WebKit, Firefox and IE >= 9)
2. Works for content in elements that are scrollable (i.e. have their own scrollbars)
3. Much more comprehensive testing/error handling
4. Properly encodes/decodes assertions
5. Handles points in the padding of elements consistently
6. Has a utility method to calculate the CFI for the current viewport position robustly
To check if this script is compatible with the current browser, call
window.cfi.is_compatible() it will throw an exception if not compatible.
Tested on: Firefox 9, IE 9, Chromium 16, Qt WebKit 2.1
###
log = (error) -> # {{{
if error
if window?.console?.log
window.console.log(error)
else if process?.stdout?.write
process.stdout.write(error + '\n')
# }}}
# CFI escaping {{{
escape_for_cfi = (raw) ->
if raw
for c in ['^', '[', ']', ',', '(', ')', ';', '~', '@', '-', '!']
raw = raw.replace(c, '^'+c)
raw
unescape_from_cfi = (raw) ->
ans = raw
if raw
dropped = false
ans = []
for c in raw
if not dropped and c == '^'
dropped = true
continue
dropped = false
ans.push(c)
ans = ans.join('')
ans
# }}}
fstr = (d) -> # {{{
# Convert a timestamp floating point number to a string
ans = ""
if ( d < 0 )
ans = "-"
d = -d
n = Math.floor(d)
ans += n
n = Math.round((d-n)*100)
if( n != 0 )
ans += "."
ans += if (n % 10 == 0) then (n/10) else n
ans
# }}}
get_current_time = (target) -> # {{{
ans = 0
if target.currentTime != undefined
ans = target.currentTime
fstr(ans)
# }}}
window_scroll_pos = (win=window) -> # {{{
if typeof(win.pageXOffset) == 'number'
x = win.pageXOffset
y = win.pageYOffset
else # IE < 9
if document.body and ( document.body.scrollLeft or document.body.scrollTop )
x = document.body.scrollLeft
y = document.body.scrollTop
else if document.documentElement and ( document.documentElement.scrollLeft or document.documentElement.scrollTop)
y = document.documentElement.scrollTop
x = document.documentElement.scrollLeft
return [x, y]
# }}}
viewport_to_document = (x, y, doc=window?.document) -> # {{{
until doc == window.document
# We are in a frame
frame = doc.defaultView.frameElement
rect = frame.getBoundingClientRect()
x += rect.left
y += rect.top
doc = frame.ownerDocument
win = doc.defaultView
[wx, wy] = window_scroll_pos(win)
x += wx
y += wy
return [x, y]
# }}}
# Convert point to character offset {{{
range_has_point = (range, x, y) ->
for rect in range.getClientRects()
if (rect.left <= x <= rect.right) and (rect.top <= y <= rect.bottom)
return true
return false
offset_in_text_node = (node, range, x, y) ->
limits = [0, node.nodeValue.length]
while limits[0] != limits[1]
pivot = Math.floor( (limits[0] + limits[1]) / 2 )
lr = [limits[0], pivot]
rr = [pivot+1, limits[1]]
range.setStart(node, pivot)
range.setEnd(node, pivot+1)
if range_has_point(range, x, y)
return pivot
range.setStart(node, rr[0])
range.setEnd(node, rr[1])
if range_has_point(range, x, y)
limits = rr
continue
range.setStart(node, lr[0])
range.setEnd(node, lr[1])
if range_has_point(range, x, y)
limits = lr
continue
break
return limits[0]
find_offset_for_point = (x, y, node, cdoc) ->
range = cdoc.createRange()
child = node.firstChild
while child
if child.nodeType in [3, 4, 5, 6] and child.nodeValue?.length
range.setStart(child, 0)
range.setEnd(child, child.nodeValue.length)
if range_has_point(range, x, y)
return [child, offset_in_text_node(child, range, x, y)]
child = child.nextSibling
# The point must be after the last bit of text/in the padding/border, we dont know
# how to get a good point in this case
throw "Point (#{x}, #{y}) is in the padding/border of #{node}, so cannot calculate offset"
# }}}
class CanonicalFragmentIdentifier
###
This class is a namespace to expose CFI functions via the window.cfi
object. The most important functions are:
is_compatible(): Throws an error if the browser is not compatible with
this script
at(x, y): Maps a point to a CFI, if possible
at_current(): Returns the CFI corresponding to the current viewport scroll location
scroll_to(cfi): which scrolls the browser to a point corresponding to the
given cfi, and returns the x and y co-ordinates of the point.
###
constructor: () -> # {{{
if not this instanceof arguments.callee
throw new Error('CFI constructor called as function')
this.CREATE_RANGE_ERR = "Your browser does not support the createRange function. Update it to a newer version."
this.IE_ERR = "Your browser is too old. You need Internet Explorer version 9 or newer."
div = document.createElement('div')
ver = 3
while true
div.innerHTML = "<!--[if gt IE #{ ++ver }]><i></i><![endif]-->"
if div.getElementsByTagName('i').length == 0
break
this.iever = ver
this.isie = ver > 4
# }}}
is_compatible: () -> # {{{
if not window.document.createRange
throw this.CREATE_RANGE_ERR
# Check if Internet Explorer >= 8 as getClientRects returns physical
# rather than logical pixels on older IE
if this.isie and this.iever < 9
# We have IE < 9
throw this.IE_ERR
# }}}
set_current_time: (target, val) -> # {{{
if target.currentTime == undefined
return
if target.readyState == 4 or target.readyState == "complete"
target.currentTime = val + 0
else
fn = ()-> target.currentTime = val
target.addEventListener("canplay", fn, false)
#}}}
encode: (doc, node, offset, tail) -> # {{{
cfi = tail or ""
# Handle the offset, if any
switch node.nodeType
when 1 # Element node
if typeof(offset) == 'number'
node = node.childNodes.item(offset)
when 3, 4, 5, 6 # Text/entity/CDATA node
offset or= 0
while true
p = node.previousSibling
if not p or p.nodeType > 8
break
# log("previous sibling:"+ p + " " + p?.nodeType + " length: " + p?.nodeValue?.length)
if p.nodeType not in [2, 8] and p.nodeValue?.length?
offset += p.nodeValue.length
node = p
cfi = ":" + offset + cfi
else # Not handled
log("Offsets for nodes of type #{ node.nodeType } are not handled")
# Construct the path to node from root
until node == doc
p = node.parentNode
if not p
if node.nodeType == 9 # Document node (iframe)
win = node.defaultView
if win.frameElement
node = win.frameElement
cfi = "!" + cfi
continue
break
# Find position of node in parent
index = 0
child = p.firstChild
while true
index |= 1 # Increment index by 1 if it is even
if child.nodeType == 1
index++
if child == node
break
child = child.nextSibling
# Add id assertions for robustness where possible
id = node.getAttribute?('id')
idspec = if id then "[#{ escape_for_cfi(id) }]" else ''
cfi = '/' + index + idspec + cfi
node = p
cfi
# }}}
decode: (cfi, doc=window?.document) -> # {{{
simple_node_regex = ///
^/(\d+) # The node count
(\[[^\]]*\])? # The optional id assertion
///
error = null
node = doc
until cfi.length < 1 or error
if (r = cfi.match(simple_node_regex)) # Path step
target = parseInt(r[1])
assertion = r[2]
if assertion
assertion = unescape_from_cfi(assertion.slice(1, assertion.length-1))
index = 0
child = node.firstChild
while true
if not child
if assertion # Try to use the assertion to find the node
child = doc.getElementById(assertion)
if child
node = child
if not child
error = "No matching child found for CFI: " + cfi
cfi = cfi.substr(r[0].length)
break
index |= 1 # Increment index by 1 if it is even
if child.nodeType == 1
index++
if index == target
cfi = cfi.substr(r[0].length)
node = child
if assertion and node.id != assertion
# The found child does not match the id assertion,
# trust the id assertion if an element with that id
# exists
child = doc.getElementById(assertion)
if child
node = child
break
child = child.nextSibling
else if cfi[0] == '!' # Indirection
if node.contentDocument
node = node.contentDocument
cfi = cfi.substr(1)
else
error = "Cannot reference #{ node.nodeName }'s content:" + cfi
else
break
if error
log(error)
return null
point = {}
error = null
offset = null
if (r = cfi.match(/^:(\d+)/)) != null
# Character offset
offset = parseInt(r[1])
cfi = cfi.substr(r[0].length)
if (r = cfi.match(/^~(-?\d+(\.\d+)?)/)) != null
# Temporal offset
point.time = r[1] - 0 # Coerce to number
cfi = cfi.substr(r[0].length)
if (r = cfi.match(/^@(-?\d+(\.\d+)?):(-?\d+(\.\d+)?)/)) != null
# Spatial offset
point.x = r[1] - 0 # Coerce to number
point.y = r[3] - 0 # Coerce to number
cfi = cfi.substr(r[0].length)
if( (r = cfi.match(/^\[([^\]]+)\]/)) != null )
assertion = r[1]
cfi = cfi.substr(r[0].length)
if (r = assertion.match(/;s=([ab])$/)) != null
if r.index > 0 and assertion[r.index - 1] != '^'
assertion = assertion.substr(0, r.index)
point.forward = (r[1] == 'a')
assertion = unescape_from_cfi(assertion)
# TODO: Handle text assertion
# Find the text node that contains the offset
node?.parentNode?.normalize()
if offset != null
while true
len = node.nodeValue.length
if offset < len or (not point.forward and offset == len)
break
next = false
while true
nn = node.nextSibling
if not nn
break
if nn.nodeType in [3, 4, 5, 6] and nn.nodeValue?.length # Text node, entity, cdata
next = nn
break
node = nn
if not next
if offset > len
error = "Offset out of range: #{ offset }"
offset = len
break
node = next
offset -= len
point.offset = offset
point.node = node
if error
point.error = error
else if cfi.length > 0
point.error = "Undecoded CFI: #{ cfi }"
log(point.error)
point
# }}}
at: (x, y, doc=window?.document) -> # {{{
# x, y are in viewport co-ordinates
cdoc = doc
target = null
cwin = cdoc.defaultView
tail = ''
offset = null
name = null
# Drill down into iframes, etc.
while true
target = cdoc.elementFromPoint x, y
if not target or target.localName in ['html', 'body']
# We ignore both html and body even though body could
# have text nodes under it as performance is very poor if body
# has large margins/padding (for e.g. in fullscreen mode)
# A possible solution for this is to wrap all text node
# children of body in <span> but that is seriously ugly and
# might have side effects. Lets do this only if there are lots of
# books in the wild that actually have text children of body,
# and even in this case it might be better to change the input
# plugin to prevent this from happening.
# log("No element at (#{ x }, #{ y })")
return null
name = target.localName
if name not in ['iframe', 'embed', 'object']
break
cd = target.contentDocument
if not cd
break
# We have an embedded document, transforms x, y into the co-prd
# system of the embedded document's viewport
rect = target.getBoundingClientRect()
x -= rect.left
y -= rect.top
cdoc = cd
cwin = cdoc.defaultView
(if target.parentNode then target.parentNode else target).normalize()
if name in ['audio', 'video']
tail = "~" + get_current_time(target)
if name in ['img', 'video']
rect = target.getBoundingClientRect()
px = ((x - rect.left)*100)/target.offsetWidth
py = ((y - rect.top)*100)/target.offsetHeight
tail = "#{ tail }@#{ fstr px }:#{ fstr py }"
else if name != 'audio'
# Get the text offset
# We use a custom function instead of caretRangeFromPoint as
# caretRangeFromPoint does weird things when the point falls in the
# padding of the element
if cdoc.createRange
[target, offset] = find_offset_for_point(x, y, target, cdoc)
else
throw this.CREATE_RANGE_ERR
this.encode(doc, target, offset, tail)
# }}}
point: (cfi, doc=window?.document) -> # {{{
r = this.decode(cfi, doc)
if not r
return null
node = r.node
ndoc = node.ownerDocument
if not ndoc
log("CFI node has no owner document: #{ cfi } #{ node }")
return null
nwin = ndoc.defaultView
x = null
y = null
range = null
if typeof(r.offset) == "number"
# Character offset
if not ndoc.createRange
throw this.CREATE_RANGE_ERR
range = ndoc.createRange()
if r.forward
try_list = [{start:0, end:0, a:0.5}, {start:0, end:1, a:1}, {start:-1, end:0, a:0}]
else
try_list = [{start:0, end:0, a:0.5}, {start:-1, end:0, a:0}, {start:0, end:1, a:1}]
a = null
rects = null
node_len = node.nodeValue.length
offset = r.offset
for i in [0, 1]
# Try reducing the offset by 1 if we get no match as if it refers to the position after the
# last character we wont get a match with getClientRects
offset = r.offset - i
if offset < 0
offset = 0
k = 0
until rects?.length or k >= try_list.length
t = try_list[k++]
start_offset = offset + t.start
end_offset = offset + t.end
a = t.a
if start_offset < 0 or end_offset >= node_len
continue
range.setStart(node, start_offset)
range.setEnd(node, end_offset)
rects = range.getClientRects()
if rects?.length
break
if not rects?.length
log("Could not find caret position: rects: #{ rects } offset: #{ r.offset }")
return null
else
[x, y] = [r.x, r.y]
{x:x, y:y, node:r.node, time:r.time, range:range, a:a}
# }}}
scroll_to: (cfi, callback=false, doc=window?.document) -> # {{{
if window.mathjax?.math_present and not window.mathjax?.math_loaded
window.mathjax.pending_cfi = [cfi, callback]
return
point = this.point(cfi, doc)
if not point
log("No point found for cfi: #{ cfi }")
return
if typeof point.time == 'number'
this.set_current_time(point.node, point.time)
if point.range != null
# Character offset
r = point.range
[so, eo, sc, ec] = [r.startOffset, r.endOffset, r.startContainer, r.endContainer]
node = r.startContainer
ndoc = node.ownerDocument
nwin = ndoc.defaultView
span = ndoc.createElement('span')
span.setAttribute('style', 'border-width: 0; padding: 0; margin: 0')
r.surroundContents(span)
span.scrollIntoView()
fn = ->
# Remove the span and get the new position now that scrolling
# has (hopefully) completed
#
# In WebKit, the boundingrect of the span is wrong in some
# situations, whereas in IE resetting the range causes it to
# loose bounding info. So we use the range's rects unless they
# are absent, in which case we use the span's rect
#
rect = span.getBoundingClientRect()
# Remove the span we inserted
p = span.parentNode
for node in span.childNodes
span.removeChild(node)
p.insertBefore(node, span)
p.removeChild(span)
p.normalize()
# Reset the range to what it was before the span was added
r.setStart(sc, so)
r.setEnd(ec, eo)
rects = r.getClientRects()
if rects.length > 0
rect = rects[0]
x = (point.a*rect.left + (1-point.a)*rect.right)
y = (rect.top + rect.bottom)/2
[x, y] = viewport_to_document(x, y, ndoc)
if callback
callback(x, y)
else
node = point.node
nwin = node.ownerDocument.defaultView
node.scrollIntoView()
fn = ->
r = node.getBoundingClientRect()
[x, y] = viewport_to_document(r.left, r.top, node.ownerDocument)
if typeof(point.x) == 'number' and node.offsetWidth
x += (point.x*node.offsetWidth)/100
if typeof(point.y) == 'number' and node.offsetHeight
y += (point.y*node.offsetHeight)/100
scrollTo(x, y)
if callback
callback(x, y)
setTimeout(fn, 10)
null
# }}}
at_point: (ox, oy) ->
# The CFI at the specified point. Different to at() in that this method
# returns null if there is an error, and also calculates a point from
# the CFI and returns null if the calculated point is far from the
# original point.
dist = (p1, p2) ->
Math.sqrt(Math.pow(p1[0]-p2[0], 2), Math.pow(p1[1]-p2[1], 2))
try
cfi = window.cfi.at(ox, oy)
point = window.cfi.point(cfi)
catch err
cfi = null
if cfi
if point.range != null
r = point.range
rect = r.getClientRects()[0]
x = (point.a*rect.left + (1-point.a)*rect.right)
y = (rect.top + rect.bottom)/2
[x, y] = viewport_to_document(x, y, r.startContainer.ownerDocument)
else
node = point.node
r = node.getBoundingClientRect()
[x, y] = viewport_to_document(r.left, r.top, node.ownerDocument)
if typeof(point.x) == 'number' and node.offsetWidth
x += (point.x*node.offsetWidth)/100
if typeof(point.y) == 'number' and node.offsetHeight
y += (point.y*node.offsetHeight)/100
if dist(viewport_to_document(ox, oy), [x, y]) > 50
cfi = null
return cfi
at_current: () -> # {{{
[winx, winy] = window_scroll_pos()
[winw, winh] = [window.innerWidth, window.innerHeight]
max = Math.max
winw = max(winw, 400)
winh = max(winh, 600)
deltay = Math.floor(winh/50)
deltax = Math.floor(winw/25)
miny = max(-winy, -winh)
maxy = winh
minx = max(-winx, -winw)
maxx = winw
x_loop = (cury) =>
for direction in [-1, 1]
delta = deltax * direction
curx = 0
until (direction < 0 and curx < minx) or (direction > 0 and curx > maxx)
cfi = this.at_point(curx, cury)
if cfi
return cfi
curx += delta
null
for direction in [-1, 1]
delta = deltay * direction
cury = 0
until (direction < 0 and cury < miny) or (direction > 0 and cury > maxy)
cfi = x_loop(cury, -1)
if cfi
return cfi
cury += delta
# Use a spatial offset on the html element, since we could not find a
# normal CFI
[x, y] = window_scroll_pos()
de = document.documentElement
rect = de.getBoundingClientRect()
px = (x*100)/rect.width
py = (y*100)/rect.height
cfi = "/2@#{ fstr px }:#{ fstr py }"
return cfi
# }}}
if window?
window.cfi = new CanonicalFragmentIdentifier()
else if process?
# Some debugging code goes here to be run with the coffee interpreter
cfi = new CanonicalFragmentIdentifier()
t = 'a^!,1'
log(t)
log(escape_for_cfi(t))
log(unescape_from_cfi(escape_for_cfi(t)))

View File

@ -1,211 +0,0 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2012, Kovid Goyal <kovid at kovidgoyal.net>
Released under the GPLv3 License
###
if window?.calibre_utils
log = window.calibre_utils.log
merge = (node, cnode) ->
rules = node.ownerDocument.defaultView.getMatchedCSSRules(node, '')
if rules
for rule in rules
style = rule.style
for name in style
val = style.getPropertyValue(name)
if val and not cnode.style.getPropertyValue(name)
cnode.style.setProperty(name, val)
inline_styles = (node) ->
cnode = node.cloneNode(true)
merge(node, cnode)
nl = node.getElementsByTagName('*')
cnl = cnode.getElementsByTagName('*')
for node, i in nl
merge(node, cnl[i])
return cnode
get_epub_type = (node, possible_values) ->
# Try to get the value of the epub:type attribute. Complex as we dont
# operate in XML mode
epub_type = node.getAttributeNS("http://www.idpf.org/2007/ops", 'type') or node.getAttribute('epub:type')
if not epub_type
for x in node.attributes # consider any xxx:type="noteref" attribute as marking a note
if x.nodeName and x.nodeValue in possible_values and x.nodeName.slice(-':type'.length) == ':type'
epub_type = x.nodeValue
break
return epub_type
get_containing_block = (node) ->
until node?.tagName?.toLowerCase() in ['p', 'div', 'li', 'td', 'h1', 'h2', 'h2', 'h3', 'h4', 'h5', 'h6', 'body']
node = node.parentNode
if not node
break
return node
trim = (str) ->
return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '')
is_footnote_link = (node, url, linked_to_anchors, prefix) ->
if not url or url.substr(0, prefix.length) != prefix
return false # Ignore non-local links
epub_type = get_epub_type(node, ['noteref'])
if epub_type and epub_type.toLowerCase() == 'noteref'
return true
if epub_type and epub_type == 'link'
return false
# Check if node or any of its first few parents have vertical-align set
[x, num] = [node, 3]
while x and num > 0
style = window.getComputedStyle(x)
if not style.display not in ['inline', 'inline-block']
break
if style.verticalAlign in ['sub', 'super', 'top', 'bottom']
return true
x = x.parentNode
num -= 1
# Check if node has a single child with the appropriate css
children = (x for x in node.childNodes when x.nodeType == Node.ELEMENT_NODE)
if children.length == 1
style = window.getComputedStyle(children[0])
if style.display in ['inline', 'inline-block'] and style.verticalAlign in ['sub', 'super', 'top', 'bottom']
text_children = (x for x in node.childNodes when x.nodeType == Node.TEXT_NODE and x.nodeValue and /\S+/.test(x.nodeValue))
if not text_children.length
return true
eid = node.getAttribute('id') or node.getAttribute('name')
if eid and linked_to_anchors.hasOwnProperty(eid)
# An <a href="..." id="..."> link that is linked back from some other
# file in the spine, most likely an endnote. We exclude links that are
# the only content of their parent block tag, as these are not likely
# to be endnotes.
cb = get_containing_block(node)
if not cb or cb.tagName.toLowerCase() == 'body'
return false
ltext = node.textContent
if not ltext
return false
ctext = cb.textContent
if not ctext
return false
if trim(ctext) == trim(ltext)
return false
return true
return false
is_epub_footnote = (node) ->
pv = ['note', 'footnote', 'rearnote']
epub_type = get_epub_type(node, pv)
if epub_type and epub_type.toLowerCase() in pv
return true
return false
block_tags = ['p', 'div', 'li', 'td', 'h1', 'h2', 'h2', 'h3', 'h4', 'h5', 'h6', 'body']
block_display_styles = ['block', 'list-item', 'table-cell', 'table']
get_note_container = (node) ->
until node.tagName.toLowerCase() in block_tags or is_epub_footnote(node) or getComputedStyle(node).display in block_display_styles
node = node.parentNode
if not node
break
return node
get_parents_and_self = (node) ->
ans = []
while node and node isnt document.body
ans.push(node)
node = node.parentNode
return ans
get_page_break = (node) ->
style = getComputedStyle(node)
ans = {}
for x in ['before', 'after']
ans[x] = style.getPropertyValue('page-break-'.concat(x)) in ['always', 'left', 'right']
return ans
hide_children = (node) ->
for child in node.childNodes
if child.nodeType == Node.ELEMENT_NODE
if child.do_not_hide
hide_children(child)
delete child.do_not_hide
else
child.style.display = 'none'
unhide_tree = (elem) ->
elem.do_not_hide = true
for c in elem.getElementsByTagName('*')
c.do_not_hide = true
class CalibreExtract
# This class is a namespace to expose functions via the
# window.calibre_extract object.
constructor: () ->
if not this instanceof arguments.callee
throw new Error('CalibreExtract constructor called as function')
this.marked_node = null
mark: (node) =>
this.marked_node = node
extract: (node=null) =>
if node == null
node = this.marked_node
cnode = inline_styles(node)
return cnode.outerHTML
is_footnote_link: (a, prefix, linked_to_anchors) ->
return is_footnote_link(a, a.href, linked_to_anchors, prefix)
show_footnote: (target, known_targets) ->
if not target
return
start_elem = document.getElementById(target)
if not start_elem
return
start_elem = get_note_container(start_elem)
for elem in get_parents_and_self(start_elem)
elem.do_not_hide = true
style = window.getComputedStyle(elem)
if style.display == 'list-item' and style.listStyleType not in ['disc', 'circle', 'square']
# We cannot display list numbers since they will be
# incorrect as we are removing siblings of this element.
elem.style.listStyleType = 'none'
if is_epub_footnote(start_elem)
unhide_tree(start_elem)
else
# Try to detect natural boundaries based on markup for this note
found_note_start = false
for elem in document.body.getElementsByTagName('*')
if found_note_start
eid = elem.getAttribute('id')
if eid != target and known_targets.hasOwnProperty(eid) and get_note_container(elem) != start_elem
console.log('Breaking footnote on anchor: ' + elem.getAttribute('id'))
delete get_note_container(elem).do_not_hide
break
pb = get_page_break(elem)
if pb['before']
console.log('Breaking footnote on page break before')
break
if pb['after']
unhide_tree(elem)
console.log('Breaking footnote on page break after')
break
elem.do_not_hide = true
else if elem is start_elem
found_note_start = true
hide_children(document.body)
location.hash = '#' + target
if window?
window.calibre_extract = new CalibreExtract()

View File

@ -1,67 +0,0 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2012, Kovid Goyal <kovid at kovidgoyal.net>
Released under the GPLv3 License
###
log = window.calibre_utils.log
class FullScreen
# This class is a namespace to expose functions via the
# window.full_screen object. The most important functions are:
constructor: () ->
if not this instanceof arguments.callee
throw new Error('FullScreen constructor called as function')
this.in_full_screen = false
this.initial_left_margin = null
this.initial_right_margin = null
save_margins: () ->
bs = document.body.style
this.initial_left_margin = bs.marginLeft
this.initial_right_margin = bs.marginRight
on: (max_text_width, max_text_height, in_paged_mode) ->
if in_paged_mode
window.paged_display.max_col_width = max_text_width
window.paged_display.max_col_height = max_text_height
else
s = document.body.style
s.maxWidth = max_text_width + 'px'
s.marginLeft = 'auto'
s.marginRight = 'auto'
window.addEventListener('click', this.handle_click, false)
off: (in_paged_mode) ->
window.removeEventListener('click', this.handle_click, false)
if in_paged_mode
window.paged_display.max_col_width = -1
window.paged_display.max_col_height = -1
else
s = document.body.style
s.maxWidth = 'none'
if this.initial_left_margin != null
s.marginLeft = this.initial_left_margin
if this.initial_right_margin != null
s.marginRight = this.initial_right_margin
handle_click: (event) ->
if event.target not in [document.documentElement, document.body] or event.button != 0
return
res = null
if window.paged_display.in_paged_mode
res = window.paged_display.click_for_page_turn(event)
else
br = document.body.getBoundingClientRect()
if not (br.left <= event.clientX <= br.right)
res = event.clientX < br.left
if res != null
window.py_bridge.page_turn_requested(res)
if window?
window.full_screen = new FullScreen()

View File

@ -1,132 +0,0 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2012, Kovid Goyal <kovid@kovidgoyal.net>
Released under the GPLv3 License
###
window_scroll_pos = (win=window) -> # {{{
if typeof(win.pageXOffset) == 'number'
x = win.pageXOffset
y = win.pageYOffset
else # IE < 9
if document.body and ( document.body.scrollLeft or document.body.scrollTop )
x = document.body.scrollLeft
y = document.body.scrollTop
else if document.documentElement and ( document.documentElement.scrollLeft or document.documentElement.scrollTop)
y = document.documentElement.scrollTop
x = document.documentElement.scrollLeft
return [x, y]
# }}}
viewport_to_document = (x, y, doc=window?.document) -> # {{{
until doc == window.document
# We are in a frame
frame = doc.defaultView.frameElement
rect = frame.getBoundingClientRect()
x += rect.left
y += rect.top
doc = frame.ownerDocument
win = doc.defaultView
[wx, wy] = window_scroll_pos(win)
x += wx
y += wy
return [x, y]
# }}}
class BookIndexing
###
This class is a namespace to expose indexing functions via the
window.book_indexing object. The most important functions are:
anchor_positions(): Get the absolute (document co-ordinate system) position
for elements with the specified id/name attributes.
###
constructor: () ->
this.cache = {}
this.last_check = [null, null]
cache_valid: (anchors) ->
if not anchors
return false
for a in anchors
if not Object.prototype.hasOwnProperty.call(this.cache, a)
return false
for p of this.cache
if Object.prototype.hasOwnProperty.call(this.cache, p) and p not in anchors
return false
return true
anchor_positions: (anchors, use_cache=false) ->
body = document.body
doc_constant = body.scrollHeight == this.last_check[1] and body.scrollWidth == this.last_check[0]
if use_cache and doc_constant and this.cache_valid(anchors)
return this.cache
ans = {}
if not anchors
return ans
for anchor in anchors
elem = document.getElementById(anchor)
if elem == null
# Look for an <a name="anchor"> element
try
result = document.evaluate(
".//*[local-name() = 'a' and @name='#{ anchor }']",
body, null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null)
elem = result.singleNodeValue
catch error
# The anchor had a ' or other invalid char
elem = null
if elem == null
pos = [body.scrollWidth+1000, body.scrollHeight+1000]
else
# Because of a bug in WebKit's getBoundingClientRect() in
# column mode, this position can be inaccurate,
# see https://bugs.launchpad.net/calibre/+bug/1132641 for a
# test case. The usual symptom of the inaccuracy is br.top is
# highly negative.
br = elem.getBoundingClientRect()
pos = viewport_to_document(br.left, br.top, elem.ownerDocument)
if window.paged_display?.in_paged_mode
pos[0] = window.paged_display.column_at(pos[0])
ans[anchor] = pos
this.cache = ans
this.last_check = [body.scrollWidth, body.scrollHeight]
return ans
all_links_and_anchors: () ->
body = document.body
links = []
anchors = {}
in_paged_mode = window.paged_display?.in_paged_mode
for a in document.querySelectorAll("body, body a[href], body [id], body a[name]")
if in_paged_mode
geom = window.paged_display.column_location(a)
else
br = a.getBoundingClientRect()
[left, top] = viewport_to_document(br.left, br.top, a.ownerDocument)
geom = {'left':left, 'top':top, 'width':br.right-br.left, 'height':br.bottom-br.top}
href = a.getAttribute('href')
if href
links.push([href, geom])
id = a.getAttribute("id")
if id and not anchors[id]
anchors[id] = geom
if a.tagName in ['A', "a"]
name = a.getAttribute("name")
if name and not anchors[name]
anchors[name] = geom
return {'links':links, 'anchors':anchors}
if window?
window.book_indexing = new BookIndexing()

View File

@ -1,129 +0,0 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2012, Kovid Goyal <kovid at kovidgoyal.net>
Released under the GPLv3 License
###
log = window.calibre_utils.log
startswith = (string, q) ->
return string.substring(0, q.length) == q
init_mathjax = () ->
orig = window.MathJax.Ajax.fileURL.bind(window.MathJax.Ajax)
window.MathJax.Ajax.fileURL = (mathjax_name) ->
ans = orig(mathjax_name)
if startswith(mathjax_name, '[MathJax]/../fonts')
ans = ans.replace('/../fonts', '/fonts')
return ans
class MathJax
# This class is a namespace to expose functions via the
# window.mathjax object. The most important functions are:
#
constructor: () ->
if not this instanceof arguments.callee
throw new Error('MathJax constructor called as function')
this.base = null
this.math_present = false
this.math_loaded = false
this.pending_cfi = null
this.hub = null
load_mathjax: (user_config, is_windows) ->
if this.base == null
log('You must specify the path to the MathJax installation before trying to load MathJax')
return null
script = document.createElement('script')
script.type = 'text/javascript'
script.onerror = (ev) ->
console.log('Failed to load MathJax script: ' + ev.target.src)
base = this.base
if base.substr(base.length - 1) != '/'
base += '/'
script.src = base + 'MathJax.js'
window.MathJax = {AuthorInit: init_mathjax}
script.text = user_config + ('''
MathJax.Hub.signal.Interest(function (message) {if (String(message).match(/error/i)) {console.log(message)}});
MathJax.Hub.Config({
positionToHash: false,
showMathMenu: false,
extensions: ["tex2jax.js", "asciimath2jax.js", "mml2jax.js"],
jax: ["input/TeX","input/MathML","input/AsciiMath","output/CommonHTML"],
TeX: {
extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
}
});
MathJax.Hub.Startup.onload();
MathJax.Hub.Register.StartupHook("End", window.mathjax.load_finished);
window.mathjax.hub = MathJax.Hub
''')
document.head.appendChild(script)
load_finished: () =>
log('MathJax load finished!')
this.math_loaded = true
if this.pending_cfi != null
[cfi, callback] = this.pending_cfi
this.pending_cfi = null
window.cfi.scroll_to(cfi, callback)
check_for_math: (is_windows) ->
script = null
this.math_present = false
this.math_loaded = false
this.pending_cfi = null
user_config = ''
for c in document.getElementsByTagName('script')
if c.getAttribute('type') == 'text/x-mathjax-config'
if c.text
user_config += c.text
script = c
c.parentNode.removeChild(c)
if script != null or document.getElementsByTagName('math').length > 0
this.math_present = true
this.remove_math_fallbacks()
this.load_mathjax(user_config, is_windows)
return this.math_present
remove_math_fallbacks: () ->
# localName no longer exists in Chrome >= 46 so you will need to
# investigate a proper solution for this in modern browsers. Probably
# use document.evaluate() and node.tagName? IE does not support
# document.evaluate() but Edge may or may not, you will need to
# experiment
for sw in document.getElementsByTagName("epub:switch")
non_math = []
found_math = false
c = sw.firstChild
while c
if c.localName == 'epub:case'
if c.getAttribute('required-namespace') == "http://www.w3.org/1998/Math/MathML"
found_math = c
else
non_math.push(c)
else if c.localName == 'epub:default'
non_math.push(c)
c = c.nextSibling
if found_math
for c in non_math
c.style.display = 'none'
after_resize: () ->
if not this.math_present or this.hub == null
return
# SVG output does not dynamically reflow on resize, so we manually
# rerender, this is slow, but neccessary for tables and equation
# numbers.
this.hub.Queue(["Rerender",this.hub])
if window?
window.mathjax = new MathJax()

View File

@ -1,672 +0,0 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2012, Kovid Goyal <kovid@kovidgoyal.net>
Released under the GPLv3 License
###
log = window.calibre_utils.log
runscripts = (parent) ->
for script in parent.getElementsByTagName('script')
eval(script.text || script.textContent || script.innerHTML || '')
first_child = (parent) ->
c = parent.firstChild
count = 0
while c?.nodeType != 1 and count < 20
c = c?.nextSibling
count += 1
if c?.nodeType == 1
return c
return null
has_start_text = (elem) ->
# Returns true if elem has some non-whitespace text before its first child
# element
for c in elem.childNodes
if c.nodeType not in [Node.TEXT_NODE, Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE]
break
if c.nodeType == Node.TEXT_NODE and c.nodeValue != null and /\S/.test(c.nodeValue)
return true
return false
create_page_div = (elem) ->
div = document.createElement('blank-page-div')
div.innerText = ' \n '
document.body.appendChild(div)
div.style.setProperty('-webkit-column-break-before', 'always')
div.style.setProperty('display', 'block')
div.style.setProperty('white-space', 'pre')
div.style.setProperty('background-color', 'transparent')
div.style.setProperty('background-image', 'none')
div.style.setProperty('border-width', '0')
div.style.setProperty('float', 'none')
div.style.setProperty('position', 'static')
class PagedDisplay
# This class is a namespace to expose functions via the
# window.paged_display object. The most important functions are:
#
# set_geometry(): sets the parameters used to layout text in paged mode
#
# layout(): causes the currently loaded document to be laid out in columns.
constructor: () ->
if not this instanceof arguments.callee
throw new Error('PagedDisplay constructor called as function')
this.set_geometry()
this.page_width = 0
this.screen_width = 0
this.side_margin = 0
this.in_paged_mode = false
this.current_margin_side = 0
this.is_full_screen_layout = false
this.max_col_width = -1
this.max_col_height = - 1
this.current_page_height = null
this.document_margins = null
this.use_document_margins = false
this.footer_template = null
this.header_template = null
this.header = null
this.footer = null
this.hf_style = null
read_document_margins: () ->
# Read page margins from the document. First checks for an @page rule.
# If that is not found, side margins are set to the side margins of the
# body element.
if this.document_margins is null
this.document_margins = {left:null, top:null, right:null, bottom:null}
tmp = document.createElement('div')
tmp.style.visibility = 'hidden'
tmp.style.position = 'absolute'
document.body.appendChild(tmp)
for sheet in document.styleSheets
if sheet.rules
for rule in sheet.rules
if rule.type == CSSRule.PAGE_RULE
for prop in ['left', 'top', 'bottom', 'right']
val = rule.style.getPropertyValue('margin-'+prop)
if val
tmp.style.height = val
pxval = parseInt(window.getComputedStyle(tmp).height)
if not isNaN(pxval)
this.document_margins[prop] = pxval
document.body.removeChild(tmp)
if this.document_margins.left is null
val = parseInt(window.getComputedStyle(document.body).marginLeft)
if not isNaN(val)
this.document_margins.left = val
if this.document_margins.right is null
val = parseInt(window.getComputedStyle(document.body).marginRight)
if not isNaN(val)
this.document_margins.right = val
set_geometry: (cols_per_screen=1, margin_top=20, margin_side=40, margin_bottom=20) ->
this.cols_per_screen = cols_per_screen
if this.use_document_margins and this.document_margins != null
this.margin_top = if this.document_margins.top != null then this.document_margins.top else margin_top
this.margin_bottom = if this.document_margins.bottom != null then this.document_margins.bottom else margin_bottom
if this.document_margins.left != null
this.margin_side = this.document_margins.left
else if this.document_margins.right != null
this.margin_side = this.document_margins.right
else
this.margin_side = margin_side
this.effective_margin_top = this.margin_top
this.effective_margin_bottom = this.margin_bottom
else
this.margin_top = margin_top
this.margin_side = margin_side
this.margin_bottom = margin_bottom
this.effective_margin_top = this.margin_top
this.effective_margin_bottom = this.margin_bottom
handle_rtl_body: (body_style) ->
if body_style.direction == "rtl"
for node in document.body.childNodes
if node.nodeType == node.ELEMENT_NODE and window.getComputedStyle(node).direction == "rtl"
node.style.setProperty("direction", "rtl")
document.body.style.direction = "ltr"
document.documentElement.style.direction = 'ltr'
layout: (is_single_page=false) ->
# start_time = new Date().getTime()
body_style = window.getComputedStyle(document.body)
bs = document.body.style
first_layout = false
if not this.in_paged_mode
# Check if the current document is a full screen layout like
# cover, if so we treat it specially.
single_screen = (document.body.scrollHeight < window.innerHeight + 75)
has_svg = document.getElementsByTagName('svg').length > 0
only_img = document.getElementsByTagName('img').length == 1 and document.getElementsByTagName('div').length < 3 and document.getElementsByTagName('p').length < 2
if only_img
has_viewport = document.head and document.head.querySelector('meta[name="viewport"]')
if has_viewport
# Has a viewport and only an img, is probably a comic, see for
# example: https://bugs.launchpad.net/bugs/1667357
single_screen = true
this.handle_rtl_body(body_style)
first_layout = true
if not single_screen and this.cols_per_screen > 1
num = this.cols_per_screen - 1
elems = document.querySelectorAll('body > *')
if elems.length == 1
# Workaround for the case when the content is wrapped in a
# 100% height <div>. This causes the generated page divs to
# not be in the correct location. See
# https://bugs.launchpad.net/bugs/1594657 for an example.
elems[0].style.height = 'auto'
while num > 0
num -= 1
create_page_div()
ww = window.innerWidth
# Calculate the column width so that cols_per_screen columns fit in the
# window in such a way the right margin of the last column is <=
# side_margin (it may be less if the window width is not a
# multiple of n*(col_width+2*side_margin).
n = this.cols_per_screen
adjust = ww - Math.floor(ww/n)*n
# Ensure that the margins are large enough that the adjustment does not
# cause them to become negative semidefinite
sm = Math.max(2*adjust, this.margin_side)
# Minimum column width, for the cases when the window is too
# narrow
col_width = Math.max(100, ((ww - adjust)/n) - 2*sm)
if this.max_col_width > 0 and col_width > this.max_col_width
# Increase the side margin to ensure that col_width is no larger
# than max_col_width
sm += Math.ceil( (col_width - this.max_col_width) / 2*n )
col_width = Math.max(100, ((ww - adjust)/n) - 2*sm)
this.col_width = col_width
this.page_width = col_width + 2*sm
this.side_margin = sm
this.screen_width = this.page_width * this.cols_per_screen
fgcolor = body_style.getPropertyValue('color')
bs.setProperty('box-sizing', 'content-box')
bs.setProperty('-webkit-column-gap', 2*sm + 'px')
bs.setProperty('-webkit-column-width', col_width + 'px')
bs.setProperty('-webkit-column-rule', '0px inset blue')
bs.setProperty('column-fill', 'auto')
# Without this, webkit bleeds the margin of the first block(s) of body
# above the columns, which causes them to effectively be added to the
# page margins (the margin collapse algorithm)
bs.setProperty('-webkit-margin-collapse', 'separate')
c = first_child(document.body)
if c != null
# Remove page breaks on the first few elements to prevent blank pages
# at the start of a chapter
c.style.setProperty('-webkit-column-break-before', 'avoid')
if c.tagName?.toLowerCase() == 'div'
c2 = first_child(c)
if c2 != null and not has_start_text(c)
# Common pattern of all content being enclosed in a wrapper
# <div>, see for example: https://bugs.launchpad.net/bugs/1366074
# In this case, we also modify the first child of the div
# as long as there was no text before it.
c2.style.setProperty('-webkit-column-break-before', 'avoid')
this.effective_margin_top = this.margin_top
this.effective_margin_bottom = this.margin_bottom
this.current_page_height = window.innerHeight - this.margin_top - this.margin_bottom
if this.max_col_height > 0 and this.current_page_height > this.max_col_height
eh = Math.ceil((this.current_page_height - this.max_col_height) / 2)
this.effective_margin_top += eh
this.effective_margin_bottom += eh
this.current_page_height -= 2 * eh
bs.setProperty('overflow', 'visible')
bs.setProperty('height', this.current_page_height + 'px')
bs.setProperty('width', (window.innerWidth - 2*sm)+'px')
bs.setProperty('padding-top', this.effective_margin_top + 'px')
bs.setProperty('padding-bottom', this.effective_margin_bottom+'px')
bs.setProperty('padding-left', sm+'px')
bs.setProperty('padding-right', sm+'px')
for edge in ['left', 'right', 'top', 'bottom']
bs.setProperty('margin-'+edge, '0px')
bs.setProperty('border-'+edge+'-width', '0px')
bs.setProperty('min-width', '0')
bs.setProperty('max-width', 'none')
bs.setProperty('min-height', '0')
bs.setProperty('max-height', 'none')
# Convert page-breaks to column-breaks
for sheet in document.styleSheets
if sheet.rules
for rule in sheet.rules
if rule.type == CSSRule.STYLE_RULE
for prop in ['page-break-before', 'page-break-after', 'page-break-inside']
val = rule.style.getPropertyValue(prop)
if val
cprop = '-webkit-column-' + prop.substr(5)
priority = rule.style.getPropertyPriority(prop)
rule.style.setProperty(cprop, val, priority)
if first_layout
# Because of a bug in webkit column mode, svg elements defined with
# width 100% are wider than body and lead to a blank page after the
# current page (when cols_per_screen == 1). Similarly img elements
# with height=100% overflow the first column
# We only set full_screen_layout if scrollWidth is in (body_width,
# 2*body_width) as if it is <= body_width scrolling will work
# anyway and if it is >= 2*body_width it can't be a full screen
# layout
body_width = document.body.offsetWidth + 2 * sm
this.is_full_screen_layout = (only_img or has_svg) and single_screen and document.body.scrollWidth > body_width and document.body.scrollWidth < 2 * body_width
if is_single_page
this.is_full_screen_layout = true
# Prevent the TAB key from shifting focus as it causes partial
# scrolling
document.documentElement.addEventListener('keydown', (evt) ->
if evt.keyCode == 9
evt.preventDefault()
)
ncols = document.body.scrollWidth / this.page_width
if ncols != Math.floor(ncols) and not this.is_full_screen_layout
# In Qt 5 WebKit will sometimes adjust the individual column widths for
# better text layout. This is allowed as per the CSS spec, so the
# only way to ensure fixed column widths is to make sure the body
# width is an exact multiple of the column widths
bs.setProperty('width', Math.floor(ncols) * this.page_width - 2 * sm)
this.in_paged_mode = true
this.current_margin_side = sm
# log('Time to layout:', new Date().getTime() - start_time)
return sm
create_header_footer: (uuid) ->
if this.header_template != null
this.header = document.createElement('div')
this.header.setAttribute('style', "overflow:hidden; display:block; position:absolute; left:#{ this.side_margin }px; top: 0px; height: #{ this.effective_margin_top }px; width: #{ this.col_width }px; margin: 0; padding: 0")
this.header.setAttribute('id', 'pdf_page_header_'+uuid)
document.body.appendChild(this.header)
if this.footer_template != null
this.footer = document.createElement('div')
this.footer.setAttribute('style', "overflow:hidden; display:block; position:absolute; left:#{ this.side_margin }px; top: #{ window.innerHeight - this.effective_margin_bottom }px; height: #{ this.effective_margin_bottom }px; width: #{ this.col_width }px; margin: 0; padding: 0")
this.footer.setAttribute('id', 'pdf_page_footer_'+uuid)
document.body.appendChild(this.footer)
if this.header != null or this.footer != null
this.hf_uuid = uuid
this.hf_style = document.createElement('style')
this.hf_style.setAttribute('type', 'text/css')
document.head.appendChild(this.hf_style)
this.update_header_footer(1)
position_header_footer: () ->
[left, top] = calibre_utils.viewport_to_document(0, 0, document.body.ownerDocument)
if this.header != null
this.header.style.setProperty('left', left+'px')
if this.footer != null
this.footer.style.setProperty('left', left+'px')
update_header_footer: (pagenum) ->
has_images = false
this.header_footer_images = []
if this.hf_style != null
if pagenum%2 == 1 then cls = "even_page" else cls = "odd_page"
this.hf_style.innerHTML = "#pdf_page_header_#{ this.hf_uuid } .#{ cls }, #pdf_page_footer_#{ this.hf_uuid } .#{ cls } { display: none }"
title = py_bridge.title()
author = py_bridge.author()
section = py_bridge.section()
tl_section = py_bridge.tl_section()
if this.header != null
this.header.innerHTML = this.header_template.replace(/_PAGENUM_/g, pagenum+"").replace(/_TITLE_/g, title+"").replace(/_AUTHOR_/g, author+"").replace(/_TOP_LEVEL_SECTION_/g, tl_section+"").replace(/_SECTION_/g, section+"")
runscripts(this.header)
for img in this.header.getElementsByTagName('img')
this.header_footer_images.push(img)
has_images = true
if this.footer != null
this.footer.innerHTML = this.footer_template.replace(/_PAGENUM_/g, pagenum+"").replace(/_TITLE_/g, title+"").replace(/_AUTHOR_/g, author+"").replace(/_TOP_LEVEL_SECTION_/g, tl_section+"").replace(/_SECTION_/g, section+"")
runscripts(this.footer)
for img in this.header.getElementsByTagName('img')
this.header_footer_images.push(img)
has_images = true
has_images
header_footer_images_loaded: () ->
for img in this.header_footer_images
if not img.complete
return false
this.header_footer_images = []
return true
fit_images: () ->
# Ensure no images are wider than the available width in a column. Note
# that this method use getBoundingClientRect() which means it will
# force a relayout if the render tree is dirty.
images = []
vimages = []
bounding_rects = []
img_tags = document.getElementsByTagName('img')
for img in img_tags
bounding_rects.push(img.getBoundingClientRect())
maxh = this.current_page_height
for i in [0...img_tags.length]
img = img_tags[i]
previously_limited = calibre_utils.retrieve(img, 'width-limited', false)
data = calibre_utils.retrieve(img, 'img-data', null)
br = bounding_rects[i]
if data == null
data = {'left':br.left, 'right':br.right, 'height':br.height, 'display': img.style.display}
calibre_utils.store(img, 'img-data', data)
left = calibre_utils.viewport_to_document(br.left, 0, doc=img.ownerDocument)[0]
col = this.column_at(left) * this.page_width
rleft = left - col - this.current_margin_side
width = br.right - br.left
rright = rleft + width
col_width = this.page_width - 2*this.current_margin_side
if previously_limited or rright > col_width
images.push([img, col_width - rleft])
previously_limited = calibre_utils.retrieve(img, 'height-limited', false)
if previously_limited or br.height > maxh
vimages.push(img)
if previously_limited
img.style.setProperty('-webkit-column-break-before', 'auto')
img.style.setProperty('display', data.display)
img.style.setProperty('-webkit-column-break-inside', 'avoid')
for [img, max_width] in images
img.style.setProperty('max-width', max_width+'px')
calibre_utils.store(img, 'width-limited', true)
for img in vimages
data = calibre_utils.retrieve(img, 'img-data', null)
img.style.setProperty('-webkit-column-break-before', 'always')
img.style.setProperty('max-height', maxh+'px')
if data.height > maxh
# This is needed to force the image onto a new page, without
# it, the webkit algorithm may still decide to split the image
# by keeping it part of its parent block
img.style.setProperty('display', 'block')
calibre_utils.store(img, 'height-limited', true)
scroll_to_pos: (frac) ->
# Scroll to the position represented by frac (number between 0 and 1)
xpos = Math.floor(document.body.scrollWidth * frac)
this.scroll_to_xpos(xpos)
scroll_to_xpos: (xpos, animated=false, notify=false, duration=1000) ->
# Scroll so that the column containing xpos is the left most column in
# the viewport
if typeof(xpos) != 'number'
log(xpos, 'is not a number, cannot scroll to it!')
return
if this.is_full_screen_layout
window.scrollTo(0, 0)
return
pos = Math.floor(xpos/this.page_width) * this.page_width
limit = document.body.scrollWidth - this.screen_width
pos = limit if pos > limit
if animated
this.animated_scroll(pos, duration=1000, notify=notify)
else
window.scrollTo(pos, 0)
scroll_to_column: (number) ->
this.scroll_to_xpos(number * this.page_width + 10)
column_at: (xpos) ->
# Return the number of the column that contains xpos
return Math.floor(xpos/this.page_width)
column_location: (elem) ->
# Return the location of elem relative to its containing column.
# WARNING: This method may cause the viewport to scroll (to workaround
# a bug in WebKit).
br = elem.getBoundingClientRect()
# Because of a bug in WebKit's getBoundingClientRect() in column
# mode, this position can be inaccurate, see
# https://bugs.launchpad.net/calibre/+bug/1202390 for a test case.
# The usual symptom of the inaccuracy is br.top is highly negative.
if br.top < -100
# We have to actually scroll the element into view to get its
# position
elem.scrollIntoView()
[left, top] = calibre_utils.viewport_to_document(elem.scrollLeft, elem.scrollTop, elem.ownerDocument)
else
[left, top] = calibre_utils.viewport_to_document(br.left, br.top, elem.ownerDocument)
c = this.column_at(left)
width = Math.min(br.right, (c+1)*this.page_width) - br.left
if br.bottom < br.top
br.bottom = window.innerHeight
height = Math.min(br.bottom, window.innerHeight) - br.top
left -= c*this.page_width
return {'column':c, 'left':left, 'top':top, 'width':width, 'height':height}
column_boundaries: () ->
# Return the column numbers at the left edge and after the right edge
# of the viewport
l = this.column_at(window.pageXOffset + 10)
return [l, l + this.cols_per_screen]
animated_scroll: (pos, duration=1000, notify=true) ->
# Scroll the window to X-position pos in an animated fashion over
# duration milliseconds. If notify is true, py_bridge.animated_scroll_done is
# called.
delta = pos - window.pageXOffset
interval = 50
steps = Math.floor(duration/interval)
step_size = Math.floor(delta/steps)
this.current_scroll_animation = {target:pos, step_size:step_size, interval:interval, notify:notify, fn: () =>
a = this.current_scroll_animation
npos = window.pageXOffset + a.step_size
completed = false
if Math.abs(npos - a.target) < Math.abs(a.step_size)
completed = true
npos = a.target
window.scrollTo(npos, 0)
if completed
if notify
window.py_bridge.animated_scroll_done()
else
setTimeout(a.fn, a.interval)
}
this.current_scroll_animation.fn()
current_pos: (frac) ->
# The current scroll position as a fraction between 0 and 1
limit = document.body.scrollWidth - window.innerWidth
if limit <= 0
return 0.0
return window.pageXOffset / limit
current_column_location: () ->
# The location of the left edge of the left most column currently
# visible in the viewport
if this.is_full_screen_layout
return 0
x = window.pageXOffset + Math.max(10, this.current_margin_side)
return Math.floor(x/this.page_width) * this.page_width
next_screen_location: () ->
# The position to scroll to for the next screen (which could contain
# more than one pages). Returns -1 if no further scrolling is possible.
if this.is_full_screen_layout
return -1
cc = this.current_column_location()
ans = cc + this.screen_width
if this.cols_per_screen > 1
width_left = document.body.scrollWidth - (window.pageXOffset + window.innerWidth)
pages_left = width_left / this.page_width
if Math.ceil(pages_left) < this.cols_per_screen
return -1 # Only blank, dummy pages left
limit = document.body.scrollWidth - window.innerWidth
if ans > limit
ans = if window.pageXOffset < limit then limit else -1
return ans
previous_screen_location: () ->
# The position to scroll to for the previous screen (which could contain
# more than one pages). Returns -1 if no further scrolling is possible.
if this.is_full_screen_layout
return -1
cc = this.current_column_location()
ans = cc - this.screen_width
if ans < 0
# We ignore small scrolls (less than 15px) when going to previous
# screen
ans = if window.pageXOffset > 15 then 0 else -1
return ans
next_col_location: () ->
# The position to scroll to for the next column (same as
# next_screen_location() if columns per screen == 1). Returns -1 if no
# further scrolling is possible.
if this.is_full_screen_layout
return -1
cc = this.current_column_location()
ans = cc + this.page_width
limit = document.body.scrollWidth - window.innerWidth
if ans > limit
ans = if window.pageXOffset < limit then limit else -1
return ans
previous_col_location: () ->
# The position to scroll to for the previous column (same as
# previous_screen_location() if columns per screen == 1). Returns -1 if
# no further scrolling is possible.
if this.is_full_screen_layout
return -1
cc = this.current_column_location()
ans = cc - this.page_width
if ans < 0
ans = if window.pageXOffset > 0 then 0 else -1
return ans
jump_to_anchor: (name) ->
# Jump to the element identified by anchor name. Ensures that the left
# most column in the viewport is the column containing the start of the
# element and that the scroll position is at the start of the column.
elem = document.getElementById(name)
if not elem
elems = document.getElementsByName(name)
if elems
elem = elems[0]
if not elem
return
if window.mathjax?.math_present
# MathJax links to children of SVG tags and scrollIntoView doesn't
# work properly for them, so if this link points to something
# inside an <svg> tag we instead scroll the parent of the svg tag
# into view.
parent = elem
while parent and parent?.tagName?.toLowerCase() != 'svg'
parent = parent.parentNode
if parent?.tagName?.toLowerCase() == 'svg'
elem = parent.parentNode
elem.scrollIntoView()
if this.in_paged_mode
# Ensure we are scrolled to the column containing elem
# Because of a bug in WebKit's getBoundingClientRect() in column
# mode, this position can be inaccurate, see
# https://bugs.launchpad.net/calibre/+bug/1132641 for a test case.
# The usual symptom of the inaccuracy is br.top is highly negative.
br = elem.getBoundingClientRect()
if br.top < -100
# This only works because of the preceding call to
# elem.scrollIntoView(). However, in some cases it gives
# inaccurate results, so we prefer the bounding client rect,
# when possible.
left = elem.scrollLeft
else
left = br.left
this.scroll_to_xpos(calibre_utils.viewport_to_document(
left+this.margin_side, elem.scrollTop, elem.ownerDocument)[0])
snap_to_selection: () ->
# Ensure that the viewport is positioned at the start of the column
# containing the start of the current selection
if this.in_paged_mode
sel = window.getSelection()
r = sel.getRangeAt(0).getBoundingClientRect()
node = sel.anchorNode
left = calibre_utils.viewport_to_document(r.left, r.top, doc=node.ownerDocument)[0]
# Ensure we are scrolled to the column containing the start of the
# selection
this.scroll_to_xpos(left+5)
jump_to_cfi: (cfi, job_id=-1) ->
# Jump to the position indicated by the specified conformal fragment
# indicator (requires the cfi.coffee library). When in paged mode, the
# scroll is performed so that the column containing the position
# pointed to by the cfi is the left most column in the viewport
window.cfi.scroll_to(cfi, (x, y) =>
if this.in_paged_mode
this.scroll_to_xpos(x)
else
window.scrollTo(0, y)
if window.py_bridge
window.py_bridge.jump_to_cfi_finished(job_id)
)
current_cfi: () ->
# The Conformal Fragment Identifier at the current position, returns
# null if it could not be calculated. Requires the cfi.coffee library.
ans = null
if not window.cfi? or (window.mathjax?.math_present and not window.mathjax?.math_loaded)
# If MathJax is loading, it is changing the DOM, so we cannot
# reliably generate a CFI
return ans
if this.in_paged_mode
c = this.current_column_location()
for x in [c, c-this.page_width, c+this.page_width]
# Try the current column, the previous column and the next
# column. Each column is tried from top to bottom.
[left, right] = [x, x + this.page_width]
if left < 0 or right > document.body.scrollWidth
continue
deltax = Math.floor(this.page_width/25)
deltay = Math.floor(window.innerHeight/25)
cury = this.effective_margin_top
until cury >= (window.innerHeight - this.effective_margin_bottom)
curx = left + this.current_margin_side
until curx >= (right - this.current_margin_side)
cfi = window.cfi.at_point(curx-window.pageXOffset, cury-window.pageYOffset)
if cfi
log('Viewport cfi:', cfi)
return cfi
curx += deltax
cury += deltay
else
try
ans = window.cfi.at_current()
if not ans
ans = null
catch err
log(err)
if ans
log('Viewport cfi:', ans)
return ans
click_for_page_turn: (event) ->
# Check if the click event should generate a page turn. Returns
# null if it should not, true if it is a backwards page turn, false if
# it is a forward page turn.
left_boundary = this.current_margin_side
right_bondary = this.screen_width - this.current_margin_side
if left_boundary > event.clientX
return true
if right_bondary < event.clientX
return false
return null
if window?
window.paged_display = new PagedDisplay()
# TODO:
# Highlight on jump_to_anchor

View File

@ -1,97 +0,0 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2011, Kovid Goyal <kovid@kovidgoyal.net>
Released under the GPLv3 License
###
log = (error) ->
if error
if window?.console?.log
window.console.log(error)
else if process?.stdout?.write
process.stdout.write(error + '\n')
show_cfi = () ->
if window.current_cfi
fn = (x, y) ->
ms = document.getElementById("marker").style
ms.display = 'block'
ms.top = y - 30 + 'px'
ms.left = x - 1 + 'px'
window.cfi.scroll_to(window.current_cfi, fn)
null
window_ypos = (pos=null) ->
if pos == null
return window.pageYOffset
window.scrollTo(0, pos)
mark_and_reload = (evt) ->
x = evt.clientX
y = evt.clientY
if evt.button == 2
return # Right mouse click, generated only in firefox
if document.elementFromPoint(x, y)?.getAttribute('id') in ['reset', 'viewport_mode']
return
# Remove image in case the click was on the image itself, we want the cfi to
# be on the underlying element
ms = document.getElementById("marker")
ms.style.display = 'none'
if document.getElementById('viewport_mode').checked
cfi = window.cfi.at_current()
window.cfi.scroll_to(cfi)
return
fn = () ->
try
window.current_cfi = window.cfi.at(x, y)
catch err
alert("Failed to calculate cfi: #{ err }")
return
if window.current_cfi
epubcfi = "epubcfi(#{ window.current_cfi })"
ypos = window_ypos()
newloc = window.location.href.replace(/#.*$/, '') + "#" + ypos + epubcfi
window.location.replace(newloc)
window.location.reload()
setTimeout(fn, 1)
null
frame_clicked = (evt) ->
iframe = evt.target.ownerDocument.defaultView.frameElement
# We know that the offset parent of the iframe is body
# So we can easily calculate the event co-ords w.r.t. the browser window
rect = iframe.getBoundingClientRect()
x = evt.clientX + rect.left
y = evt.clientY + rect.top
mark_and_reload({'clientX':x, 'clientY':y, 'button':evt.button})
window.onload = ->
try
window.cfi.is_compatible()
catch error
alert(error)
return
document.onclick = mark_and_reload
for iframe in document.getElementsByTagName("iframe")
iframe.contentWindow.document.onclick = frame_clicked
r = location.hash.match(/#(\d*)epubcfi\((.+)\)$/)
if r
window.current_cfi = r[2]
ypos = if r[1] then 0+r[1] else 0
base = document.getElementById('first-h1').innerHTML
document.title = base + ": " + window.current_cfi
fn = () ->
show_cfi()
window_ypos(ypos)
setTimeout(fn, 100)
null

View File

@ -1,74 +0,0 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import hash_literals
from cfi import scroll_to, at_current, at
def show_cfi():
if window.current_cfi:
scroll_to(window.current_cfi, def(x, y):
ms = document.getElementById("marker").style
ms.display = 'block'
ms.top = y - 30 + 'px'
ms.left = x - 1 + 'px'
)
def mark_and_reload(evt):
x = evt.clientX
y = evt.clientY
if evt.button is 2:
return # Right mouse click, generated only in firefox
elem = document.elementFromPoint(x, y)
if elem and elem.getAttribute('id') in ['reset', 'viewport_mode']:
return
# Remove image in case the click was on the image itself, we want the cfi to
# be on the underlying element
ms = document.getElementById("marker")
ms.style.display = 'none'
if document.getElementById('viewport_mode').checked:
cfi = at_current()
scroll_to(cfi)
return
def fn():
try:
window.current_cfi = at(x, y)
except Exception as err:
alert(str.format("Failed to calculate cfi: {}", err.message))
return
if window.current_cfi:
epubcfi = 'epubcfi(' + window.current_cfi + ')'
ypos = window.pageYOffset
newloc = window.location.href.replace(/#.*$/, '') + "#" + ypos + epubcfi
window.location.replace(newloc)
window.location.reload()
setTimeout(fn, 1)
def frame_clicked(evt):
iframe = evt.target.ownerDocument.defaultView.frameElement
# We know that the offset parent of the iframe is body
# So we can easily calculate the event co-ords w.r.t. the browser window
rect = iframe.getBoundingClientRect()
x = evt.clientX + rect.left
y = evt.clientY + rect.top
mark_and_reload({'clientX':x, 'clientY':y, 'button':evt.button})
window.onload = def():
document.onclick = mark_and_reload
for iframe in document.getElementsByTagName("iframe"):
iframe.contentWindow.document.onclick = frame_clicked
r = window.location.hash.match(/#(\d*)epubcfi\((.+)\)$/)
if r:
window.current_cfi = r[2]
ypos = 0+r[1] if r[1] else 0
base = document.getElementById('first-h1').innerHTML
document.title = base + ": " + window.current_cfi
setTimeout(def():
show_cfi()
window.scrollTo(0, ypos)
, 100)

View File

@ -1,32 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>Testing EPUB CFI</title>
</head>
<body>
<p id="whitespace">But I must explain to you how all this mistaken
idea of denouncing pleasure and praising pain was born and I will
give you a complete account of the system, and expound the actual
teachings of the great explorer of the truth, the master-builder of
human happiness. No one rejects, dislikes, or avoids pleasure
itself, because it is pleasure, but because those who do not know
how to pursue pleasure rationally encounter consequences that are
extremely painful. Nor again is there anyone who <b>loves or
pursues or desires</b> to obtain pain of itself, because it is
pain, but because occasionally circumstances occur in which toil
and pain can procure him some great pleasure. To take a trivial
example, which of us ever undertakes laborious physical exercise,
except to obtain some advantage from it? But who has any right to
find fault with a man who chooses to enjoy a pleasure that has no
annoying consequences, or one who avoids a pain that produces no
resultant pleasure? On the other hand, we denounce with righteous
indignation and dislike men who are so beguiled and demoralized by
the charms of pleasure of the moment, so blinded by desire, that
they cannot foresee</p>
<p><img src="marker.png" width="300" height="300" alt="Test image"/></p>
</body>
</html>

View File

@ -1,150 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Testing cfi.coffee</title>
<meta charset="UTF-8"/>
<script type="text/javascript" src="cfi-test.js"></script>
<script type="text/javascript" src="cfi.coffee"></script>
<script type="text/javascript" src="cfi-test.coffee"></script>
<style type="text/css">
body {
font-family: sans-serif;
background-color: white;
padding-bottom: 500px;
}
h1, h2 { color: #005a9c }
h2 {
border-top: solid 2px #005a9c;
margin-top: 4ex;
}
#container {
max-width: 30em;
margin-right: auto;
margin-left: 2em;
position:relative;
}
#overflow {
max-height: 100px;
overflow: scroll;
border: solid 1px black;
padding: 2em;
}
#whitespace {
border: 20px solid gray;
margin: 20px;
padding: 20px;
}
#reset {
color: blue;
text-decoration: none
}
#reset:hover { color: red }
</style>
</head>
<body>
<div id="container">
<h1 id="first-h1">Testing cfi.coffee</h1>
<p>Click anywhere and the location will be marked with a marker, whose position is set via a CFI.</p>
<p>
<a id="reset" href="/">Reset CFI to None</a>
&nbsp;
Test viewport location calculation:
<input type="checkbox" id="viewport_mode" title=
"Checking this will cause the window to scroll to a position based on a CFI calculated for the windows current position."/>
</p>
<h2>A div with scrollbars</h2>
<p>Scroll down and click on some elements. Make sure to hit both
bold and not bold text as well as different points on the image</p>
<div id="overflow">But I must explain to you how all this mistaken
idea of denouncing pleasure and praising pain was born and I
will give you a complete account of the system, and expound the
actual teachings of the great explorer of the truth, the
master-builder of human happiness. No one rejects, dislikes, or
avoids pleasure itself, because it is pleasure, but because
those who do not know how to pursue pleasure rationally
encounter consequences that are extremely painful. Nor again is
there anyone who <b>loves or pursues or desires</b> to obtain
pain of itself, because it is pain, but because occasionally
circumstances occur in which toil and pain can procure him some
great pleasure. To take a trivial example, which of us ever
undertakes laborious physical exercise, except to obtain some
advantage from it? But who has any right to find fault with a
man who chooses to enjoy a pleasure that has no annoying
consequences, or one who avoids a pain that produces no
resultant pleasure? On the other hand, we denounce with
righteous indignation and dislike men who are so beguiled and
demoralized by the charms of pleasure of the moment, so blinded
by desire, that they cannot foresee
<img src="marker.png" width="150" height="200" alt="Test Image"
style="border: solid 1px black; display:block"/>
</div>
<h2>Some entities and comments</h2>
<p>Entities: &amp; &copy; &sect; &gt; some text after entities</p>
<p>An invisible Comment: <!-- aaaaaa --> followed by some text</p>
<p>An invalid (in HTML) CDATA: <![CDATA[CDATA]]> followed by some text</p>
<h2>Margins padding borders</h2>
<p>Try clicking in the margins, borders and padding. CFI
calculation should fail.</p>
<p id="whitespace">But I must explain to you how all this mistaken
idea of denouncing pleasure and praising pain was born and I will
give you a complete account of the system, and expound the actual
teachings of the great explorer of the truth, the master-builder of
human happiness. No one rejects, dislikes, or avoids pleasure
itself, because it is pleasure, but because those who do not know
how to pursue pleasure rationally encounter consequences that are
extremely painful. Nor again is there anyone who <b>loves or
pursues or desires</b> to obtain pain of itself, because it is
pain, but because occasionally circumstances occur in which toil
and pain can procure him some great pleasure. To take a trivial
example, which of us ever undertakes laborious physical exercise,
except to obtain some advantage from it? But who has any right to
find fault with a man who chooses to enjoy a pleasure that has no
annoying consequences, or one who avoids a pain that produces no
resultant pleasure? On the other hand, we denounce with righteous
indignation and dislike men who are so beguiled and demoralized by
the charms of pleasure of the moment, so blinded by desire, that
they cannot foresee</p>
<h2>Lots of collapsed whitespace</h2>
<p>Try clicking the A character after the colon:
A suffix</p>
<h2>Lots of nested/sibling tags</h2>
<p>A <span>bunch of <span>nested<span> and</span> <span>sibling</span>
tags, all </span> mixed together</span>. <span>Click all</span>
over this paragraph to test<span> things.</span></p>
<h2>Images</h2>
<p>Try clicking at different points along the image. Also try
changing the magnification and then hitting reload.</p>
<img src="marker.png" width="150" height="200" alt="Test Image"
style="border: solid 1px black"/>
<h2>Iframes</h2>
<p>Try clicking anywhere in the iframe below:</p>
<iframe src="iframe.html"></iframe>
<h2>Video</h2>
<p>Try clicking on this video while it is playing. The page should
reload with the video paused at the point it was at when you
clicked. To play the video you should right click on it and select
play (otherwise the click will cause a reload).
</p>
<video width="320" height="240" controls="controls" preload="auto"
src="birds.webm" type="video/webm"></video>
</div>
<img id="marker" style="position: absolute; display:none; z-index:10"
src="marker.png" alt="Marker" />
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 B

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
try:
from calibre.utils.serve_coffee import serve
except ImportError:
import init_calibre
if False:
init_calibre, serve
from calibre.utils.serve_coffee import serve
def run_devel_server():
os.chdir(os.path.dirname(os.path.abspath(__file__)))
serve(resources={'cfi.coffee':'../cfi.coffee', '/':'index.html'})
if __name__ == '__main__':
run_devel_server()

View File

@ -1,40 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, shutil, tempfile
from polyglot import socketserver
from polyglot.http_server import SimpleHTTPRequestHandler
def run_devel_server():
base = os.path.dirname(os.path.abspath(__file__))
tdir = tempfile.gettempdir()
dest = os.path.join(tdir, os.path.basename(base))
if os.path.exists(dest):
shutil.rmtree(dest)
shutil.copytree(base, dest)
for i in range(5):
base = os.path.dirname(base)
shutil.copy(os.path.join(base, 'pyj', 'read_book', 'cfi.pyj'), dest)
os.chdir(dest)
from calibre.utils.rapydscript import compile_pyj
with lopen('cfi-test.pyj', 'rb') as f, lopen('cfi-test.js', 'wb') as js:
js.write(compile_pyj(f.read()).encode('utf-8'))
PORT = 8000
Handler = SimpleHTTPRequestHandler
httpd = socketserver.TCPServer(("", PORT), Handler)
print('Serving CFI test at http://localhost:%d' % PORT)
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
if __name__ == '__main__':
run_devel_server()

View File

@ -1,151 +0,0 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2012, Kovid Goyal <kovid@kovidgoyal.net>
Released under the GPLv3 License
###
class CalibreUtils
# This class is a namespace to expose functions via the
# window.calibre_utils object.
constructor: () ->
if not this instanceof arguments.callee
throw new Error('CalibreUtils constructor called as function')
this.dom_attr = 'calibre_f3fa75ca98eb4413a4ee413f20f60226'
this.dom_data = []
# Data API {{{
retrieve: (node, key, def=null) ->
# Retrieve data previously stored on node (a DOM node) with key (a
# string). If no such data is found then return the value of def.
idx = parseInt(node.getAttribute(this.dom_attr))
if isNaN(idx)
return def
data = this.dom_data[idx]
if not data.hasOwnProperty(key)
return def
return data[key]
store: (node, key, val) ->
# Store arbitrary javscript object val on DOM node node with key (a
# string). This can be later retrieved by the retrieve method.
idx = parseInt(node.getAttribute(this.dom_attr))
if isNaN(idx)
idx = this.dom_data.length
node.setAttribute(this.dom_attr, idx+'')
this.dom_data.push({})
this.dom_data[idx][key] = val
# }}}
log: (args...) -> # {{{
# Output args to the window.console object. args are automatically
# coerced to strings
if args
msg = args.join(' ')
if window?.console?.log
window.console.log(msg)
else if process?.stdout?.write
process.stdout.write(msg + '\n')
# }}}
stack_trace: () -> # {{{
currentFunction = arguments.callee.caller
while (currentFunction)
fn = currentFunction.toString()
this.log(fn)
currentFunction = currentFunction.caller
# }}}
window_scroll_pos: (win=window) -> # {{{
# The current scroll position of the browser window
if typeof(win.pageXOffset) == 'number'
x = win.pageXOffset
y = win.pageYOffset
else # IE < 9
if document.body and ( document.body.scrollLeft or document.body.scrollTop )
x = document.body.scrollLeft
y = document.body.scrollTop
else if document.documentElement and ( document.documentElement.scrollLeft or document.documentElement.scrollTop)
y = document.documentElement.scrollTop
x = document.documentElement.scrollLeft
return [x, y]
# }}}
viewport_to_document: (x, y, doc=window?.document) -> # {{{
# Convert x, y from the viewport (window) co-ordinate system to the
# document (body) co-ordinate system
until doc == window.document
# We are in a frame
frame = doc.defaultView.frameElement
rect = frame.getBoundingClientRect()
x += rect.left
y += rect.top
doc = frame.ownerDocument
win = doc.defaultView
[wx, wy] = this.window_scroll_pos(win)
x += wx
y += wy
return [x, y]
# }}}
absleft: (elem) -> # {{{
# The left edge of elem in document co-ords. Works in all
# circumstances, including column layout. Note that this will cause
# a relayout if the render tree is dirty. Also, because of a bug in the
# version of WebKit bundled with Qt 4.8, this does not always work, see
# https://bugs.launchpad.net/bugs/1132641 for a test case.
r = elem.getBoundingClientRect()
return this.viewport_to_document(r.left, 0, elem.ownerDocument)[0]
# }}}
abstop: (elem) -> # {{{
# The left edge of elem in document co-ords. Works in all
# circumstances, including column layout. Note that this will cause
# a relayout if the render tree is dirty. Also, because of a bug in the
# version of WebKit bundled with Qt 4.8, this does not always work, see
# https://bugs.launchpad.net/bugs/1132641 for a test case.
r = elem.getBoundingClientRect()
return this.viewport_to_document(r.top, 0, elem.ownerDocument)[0]
# }}}
word_at_point: (x, y) -> # {{{
# Return the word at the specified point (in viewport co-ordinates)
range = if document.caretPositionFromPoint then document.caretPositionFromPoint(x, y) else document.caretRangeFromPoint(x, y)
if range == null
return null
node = range.startContainer
if node?.nodeType != Node.TEXT_NODE
return null
offset = range.startOffset
range = document.createRange()
range.selectNodeContents(node)
try
range.setStart(node, offset)
range.setEnd(node, offset+1)
catch error # Happens if offset is invalid
null
range.expand('word')
ans = range.toString().trim()
range.detach()
matches = ans.split(/\b/)
return if matches.length > 0 then matches[0] else null
# }}}
setup_epub_reading_system: (name, version, layout, features) -> # {{{
window.navigator.epubReadingSystem = {
'name':name, 'version':version, 'layoutStyle':layout,
'hasFeature': (feature, version=1.0) ->
if (version == null or version == 1.0) and feature.toLowerCase() in features
return true
return false
}
# }}}
if window?
window.calibre_utils = new CalibreUtils()

View File

@ -1346,23 +1346,6 @@ def event_type_name(ev_or_etype):
return 'UnknownEventType'
def secure_web_page(qwebpage_or_qwebsettings):
from PyQt5.QtWebKit import QWebSettings
settings = qwebpage_or_qwebsettings if isinstance(qwebpage_or_qwebsettings, QWebSettings) else qwebpage_or_qwebsettings.settings()
settings.setAttribute(QWebSettings.JavaEnabled, False)
settings.setAttribute(QWebSettings.PluginsEnabled, False)
settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, False)
settings.setAttribute(QWebSettings.JavascriptCanAccessClipboard, False)
settings.setAttribute(QWebSettings.LocalContentCanAccessFileUrls, False) # ensure javascript cannot read from local files
settings.setAttribute(QWebSettings.NotificationsEnabled, False)
settings.setThirdPartyCookiePolicy(QWebSettings.AlwaysBlockThirdPartyCookies)
settings.setAttribute(QWebSettings.OfflineStorageDatabaseEnabled, False)
settings.setAttribute(QWebSettings.LocalStorageEnabled, False)
QWebSettings.setOfflineStorageDefaultQuota(0)
QWebSettings.setOfflineStoragePath(None)
return settings
empty_model = QStringListModel([''])
empty_index = empty_model.index(0)

View File

@ -90,4 +90,4 @@ Highlighter = create_highlighter('JavascriptHighlighter', JavascriptLexer)
if __name__ == '__main__':
from calibre.gui2.tweak_book.editor.widget import launch_editor
launch_editor(P('viewer/images.js'), syntax='javascript')
launch_editor(P('viewer.js'), syntax='javascript')

View File

@ -1,235 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import json
from PyQt5.Qt import (
Qt, QListWidget, QListWidgetItem, QItemSelectionModel, QAction,
QGridLayout, QPushButton, QIcon, QWidget, pyqtSignal, QLabel)
from calibre.gui2 import choose_save_file, choose_files
from calibre.utils.icu import sort_key
from polyglot.builtins import unicode_type, range
class BookmarksList(QListWidget):
changed = pyqtSignal()
bookmark_activated = pyqtSignal(object)
def __init__(self, parent=None):
QListWidget.__init__(self, parent)
self.setDragEnabled(True)
self.setDragDropMode(self.InternalMove)
self.setDefaultDropAction(Qt.MoveAction)
self.setAlternatingRowColors(True)
self.setStyleSheet('QListView::item { padding: 0.5ex }')
self.viewport().setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.setContextMenuPolicy(Qt.ActionsContextMenu)
self.ac_edit = ac = QAction(QIcon(I('edit_input.png')), _('Edit this bookmark'), self)
self.addAction(ac)
self.ac_delete = ac = QAction(QIcon(I('trash.png')), _('Remove this bookmark'), self)
self.addAction(ac)
self.ac_sort = ac = QAction(_('Sort by name'), self)
self.addAction(ac)
self.ac_sort_pos = ac = QAction(_('Sort by position in book'), self)
self.addAction(ac)
def dropEvent(self, ev):
QListWidget.dropEvent(self, ev)
if ev.isAccepted():
self.changed.emit()
def keyPressEvent(self, ev):
if ev.key() in (Qt.Key_Enter, Qt.Key_Return):
i = self.currentItem()
if i is not None:
self.bookmark_activated.emit(i)
ev.accept()
return
if ev.key() in (Qt.Key_Delete, Qt.Key_Backspace):
i = self.currentItem()
if i is not None:
self.ac_delete.trigger()
ev.accept()
return
return QListWidget.keyPressEvent(self, ev)
class BookmarkManager(QWidget):
edited = pyqtSignal(object)
activated = pyqtSignal(object)
create_requested = pyqtSignal()
def __init__(self, parent):
QWidget.__init__(self, parent)
self.l = l = QGridLayout(self)
l.setContentsMargins(0, 0, 0, 0)
self.setLayout(l)
self.bookmarks_list = bl = BookmarksList(self)
bl.itemChanged.connect(self.item_changed)
l.addWidget(bl, 0, 0, 1, -1)
bl.itemClicked.connect(self.item_activated)
bl.bookmark_activated.connect(self.item_activated)
bl.changed.connect(lambda : self.edited.emit(self.get_bookmarks()))
bl.ac_edit.triggered.connect(self.edit_bookmark)
bl.ac_sort.triggered.connect(self.sort_by_name)
bl.ac_sort_pos.triggered.connect(self.sort_by_pos)
bl.ac_delete.triggered.connect(self.delete_bookmark)
self.la = la = QLabel(_(
'Double click to edit and drag-and-drop to re-order the bookmarks'))
la.setWordWrap(True)
l.addWidget(la, l.rowCount(), 0, 1, -1)
self.button_new = b = QPushButton(QIcon(I('bookmarks.png')), _('&New'), self)
b.clicked.connect(self.create_requested)
b.setToolTip(_('Create a new bookmark at the current location'))
l.addWidget(b)
self.button_delete = b = QPushButton(QIcon(I('trash.png')), _('&Remove'), self)
b.setToolTip(_('Remove the currently selected bookmark'))
b.clicked.connect(self.delete_bookmark)
l.addWidget(b, l.rowCount() - 1, 1)
self.button_delete = b = QPushButton(_('Sort by &name'), self)
b.setToolTip(_('Sort bookmarks by name'))
b.clicked.connect(self.sort_by_name)
l.addWidget(b)
self.button_delete = b = QPushButton(_('Sort by &position'), self)
b.setToolTip(_('Sort bookmarks by position in book'))
b.clicked.connect(self.sort_by_pos)
l.addWidget(b, l.rowCount() - 1, 1)
self.button_export = b = QPushButton(QIcon(I('back.png')), _('E&xport'), self)
b.clicked.connect(self.export_bookmarks)
l.addWidget(b)
self.button_import = b = QPushButton(QIcon(I('forward.png')), _('&Import'), self)
b.clicked.connect(self.import_bookmarks)
l.addWidget(b, l.rowCount() - 1, 1)
def item_activated(self, item):
bm = self.item_to_bm(item)
self.activated.emit(bm)
def set_bookmarks(self, bookmarks=()):
self.bookmarks_list.clear()
for bm in bookmarks:
if bm['title'] != 'calibre_current_page_bookmark':
i = QListWidgetItem(bm['title'])
i.setData(Qt.UserRole, self.bm_to_item(bm))
i.setFlags(i.flags() | Qt.ItemIsEditable)
self.bookmarks_list.addItem(i)
if self.bookmarks_list.count() > 0:
self.bookmarks_list.setCurrentItem(self.bookmarks_list.item(0), QItemSelectionModel.ClearAndSelect)
def set_current_bookmark(self, bm):
for i, q in enumerate(self):
if bm == q:
l = self.bookmarks_list
item = l.item(i)
l.setCurrentItem(item, QItemSelectionModel.ClearAndSelect)
l.scrollToItem(item)
def __iter__(self):
for i in range(self.bookmarks_list.count()):
yield self.item_to_bm(self.bookmarks_list.item(i))
def item_changed(self, item):
self.bookmarks_list.blockSignals(True)
title = unicode_type(item.data(Qt.DisplayRole))
if not title:
title = _('Unknown')
item.setData(Qt.DisplayRole, title)
bm = self.item_to_bm(item)
bm['title'] = title
item.setData(Qt.UserRole, self.bm_to_item(bm))
self.bookmarks_list.blockSignals(False)
self.edited.emit(self.get_bookmarks())
def delete_bookmark(self):
row = self.bookmarks_list.currentRow()
if row > -1:
self.bookmarks_list.takeItem(row)
self.edited.emit(self.get_bookmarks())
def edit_bookmark(self):
item = self.bookmarks_list.currentItem()
if item is not None:
self.bookmarks_list.editItem(item)
def sort_by_name(self):
bm = self.get_bookmarks()
bm.sort(key=lambda x:sort_key(x['title']))
self.set_bookmarks(bm)
self.edited.emit(bm)
def sort_by_pos(self):
from calibre.ebooks.epub.cfi.parse import cfi_sort_key
def pos_key(b):
if b.get('type', None) == 'cfi':
return b['spine'], cfi_sort_key(b['pos'])
return (None, None)
bm = self.get_bookmarks()
bm.sort(key=pos_key)
self.set_bookmarks(bm)
self.edited.emit(bm)
def bm_to_item(self, bm):
return bm.copy()
def item_to_bm(self, item):
return item.data(Qt.UserRole).copy()
def get_bookmarks(self):
return list(self)
def export_bookmarks(self):
filename = choose_save_file(
self, 'export-viewer-bookmarks', _('Export bookmarks'),
filters=[(_('Saved bookmarks'), ['calibre-bookmarks'])], all_files=False, initial_filename='bookmarks.calibre-bookmarks')
if filename:
data = json.dumps(self.get_bookmarks(), indent=True)
if not isinstance(data, bytes):
data = data.encode('utf-8')
with lopen(filename, 'wb') as fileobj:
fileobj.write(data)
def import_bookmarks(self):
files = choose_files(self, 'export-viewer-bookmarks', _('Import bookmarks'),
filters=[(_('Saved bookmarks'), ['calibre-bookmarks'])], all_files=False, select_only_single_file=True)
if not files:
return
filename = files[0]
imported = None
with lopen(filename, 'rb') as fileobj:
imported = json.load(fileobj)
if imported is not None:
bad = False
try:
for bm in imported:
if 'title' not in bm:
bad = True
break
except Exception:
pass
if not bad:
bookmarks = self.get_bookmarks()
for bm in imported:
if bm not in bookmarks:
bookmarks.append(bm)
self.set_bookmarks([bm for bm in bookmarks if bm['title'] != 'calibre_current_page_bookmark'])
self.edited.emit(self.get_bookmarks())

View File

@ -1,456 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import zipfile
from functools import partial
from PyQt5.Qt import (
QFont, QDialog, Qt, QColor, QColorDialog, QMenu, QInputDialog,
QListWidgetItem, QFormLayout, QLabel, QLineEdit, QDialogButtonBox)
from calibre.constants import isxp
from calibre.utils.config import Config, StringConfig, JSONConfig
from calibre.utils.icu import sort_key
from calibre.utils.localization import get_language, calibre_langcode_to_name
from calibre.gui2 import min_available_height, error_dialog
from calibre.gui2.languages import LanguagesEdit
from calibre.gui2.shortcuts import ShortcutConfig
from calibre.gui2.viewer.config_ui import Ui_Dialog
from polyglot.builtins import iteritems, unicode_type
def config(defaults=None):
desc = _('Options to customize the e-book viewer')
if defaults is None:
c = Config('viewer', desc)
else:
c = StringConfig(defaults, desc)
c.add_opt('remember_window_size', default=False,
help=_('Remember last used window size'))
c.add_opt('user_css', default='',
help=_('Set the user CSS stylesheet. This can be used to customize the look of all books.'))
c.add_opt('max_fs_width', default=800,
help=_("Set the maximum width that the book's text and pictures will take"
" when in fullscreen mode. This allows you to read the book text"
" without it becoming too wide."))
c.add_opt('max_fs_height', default=-1,
help=_("Set the maximum height that the book's text and pictures will take"
" when in fullscreen mode. This allows you to read the book text"
" without it becoming too tall. Note that this setting only takes effect in paged mode (which is the default mode)."))
c.add_opt('fit_images', default=True,
help=_('Resize images larger than the viewer window to fit inside it'))
c.add_opt('hyphenate', default=False, help=_('Hyphenate text'))
c.add_opt('hyphenate_default_lang', default='en',
help=_('Default language for hyphenation rules'))
c.add_opt('search_online_url', default='https://www.google.com/search?q={text}',
help=_('The URL to use when searching for selected text online'))
c.add_opt('remember_current_page', default=True,
help=_('Save the current position in the document, when quitting'))
c.add_opt('copy_bookmarks_to_file', default=True,
help=_('Copy bookmarks to the e-book file for easy sharing, if possible'))
c.add_opt('wheel_flips_pages', default=False,
help=_('Have the mouse wheel turn pages'))
c.add_opt('wheel_scroll_fraction', default=100,
help=_('Control how much the mouse wheel scrolls by in flow mode'))
c.add_opt('line_scroll_fraction', default=100,
help=_('Control how much the arrow keys scroll by in flow mode'))
c.add_opt('tap_flips_pages', default=True,
help=_('Tapping on the screen turns pages'))
c.add_opt('line_scrolling_stops_on_pagebreaks', default=False,
help=_('Prevent the up and down arrow keys from scrolling past '
'page breaks'))
c.add_opt('page_flip_duration', default=0.5,
help=_('The time, in seconds, for the page flip animation. Default'
' is half a second.'))
c.add_opt('font_magnification_step', default=0.2,
help=_('The amount by which to change the font size when clicking'
' the font larger/smaller buttons. Should be a number between '
'0 and 1.'))
c.add_opt('fullscreen_clock', default=False, action='store_true',
help=_('Show a clock in fullscreen mode.'))
c.add_opt('fullscreen_pos', default=False, action='store_true',
help=_('Show reading position in fullscreen mode.'))
c.add_opt('fullscreen_scrollbar', default=True, action='store_false',
help=_('Show the scrollbar in fullscreen mode.'))
c.add_opt('start_in_fullscreen', default=False, action='store_true',
help=_('Start viewer in full screen mode'))
c.add_opt('show_fullscreen_help', default=True, action='store_false',
help=_('Show full screen usage help'))
c.add_opt('cols_per_screen', default=1)
c.add_opt('cols_per_screen_portrait', default=1)
c.add_opt('cols_per_screen_landscape', default=1)
c.add_opt('cols_per_screen_migrated', default=False, action='store_true')
c.add_opt('use_book_margins', default=False, action='store_true')
c.add_opt('top_margin', default=20)
c.add_opt('side_margin', default=40)
c.add_opt('bottom_margin', default=20)
c.add_opt('text_color', default=None)
c.add_opt('background_color', default=None)
c.add_opt('show_controls', default=True)
fonts = c.add_group('FONTS', _('Font options'))
fonts('serif_family', default='Liberation Serif',
help=_('The serif font family'))
fonts('sans_family', default='Liberation Sans',
help=_('The sans-serif font family'))
fonts('mono_family', default='Liberation Mono',
help=_('The monospace font family'))
fonts('default_font_size', default=20, help=_('The standard font size in px'))
fonts('mono_font_size', default=16, help=_('The monospace font size in px'))
fonts('standard_font', default='serif', help=_('The standard font type'))
fonts('minimum_font_size', default=8, help=_('The minimum font size in px'))
oparse = c.parse
def parse():
ans = oparse()
if not ans.cols_per_screen_migrated:
ans.cols_per_screen_portrait = ans.cols_per_screen_landscape = ans.cols_per_screen
return ans
c.parse = parse
return c
def load_themes():
return JSONConfig('viewer_themes')
class ConfigDialog(QDialog, Ui_Dialog):
def __init__(self, shortcuts, parent=None):
QDialog.__init__(self, parent)
self.setupUi(self)
for x in ('text', 'background'):
getattr(self, 'change_%s_color_button'%x).clicked.connect(
partial(self.change_color, x, reset=False))
getattr(self, 'reset_%s_color_button'%x).clicked.connect(
partial(self.change_color, x, reset=True))
self.css.setToolTip(_('Set the user CSS stylesheet. This can be used to customize the look of all books.'))
self.shortcuts = shortcuts
self.shortcut_config = ShortcutConfig(shortcuts, parent=self)
bb = self.buttonBox
bb.button(bb.RestoreDefaults).clicked.connect(self.restore_defaults)
with zipfile.ZipFile(P('viewer/hyphenate/patterns.zip',
allow_user_override=False), 'r') as zf:
pats = [x.split('.')[0].replace('-', '_') for x in zf.namelist()]
lang_pats = {
'el_monoton': get_language('el').partition(';')[0] + _(' monotone'), 'el_polyton':get_language('el').partition(';')[0] + _(' polytone'),
'sr_cyrl': get_language('sr') + _(' cyrillic'), 'sr_latn': get_language('sr') + _(' latin'),
}
def gl(pat):
return lang_pats.get(pat, get_language(pat))
names = list(map(gl, pats))
pmap = {}
for i in range(len(pats)):
pmap[names[i]] = pats[i]
for x in sorted(names):
self.hyphenate_default_lang.addItem(x, pmap[x])
self.hyphenate_pats = pats
self.hyphenate_names = names
p = self.tabs.widget(1)
p.layout().addWidget(self.shortcut_config)
if isxp:
self.hyphenate.setVisible(False)
self.hyphenate_default_lang.setVisible(False)
self.hyphenate_label.setVisible(False)
self.themes = load_themes()
self.save_theme_button.clicked.connect(self.save_theme)
self.load_theme_button.m = m = QMenu()
self.load_theme_button.setMenu(m)
m.triggered.connect(self.load_theme)
self.delete_theme_button.m = m = QMenu()
self.delete_theme_button.setMenu(m)
m.triggered.connect(self.delete_theme)
opts = config().parse()
self.load_options(opts)
self.init_load_themes()
self.init_dictionaries()
self.clear_search_history_button.clicked.connect(self.clear_search_history)
self.resize(self.width(), min(self.height(), max(575, min_available_height()-25)))
for x in 'add remove change'.split():
getattr(self, x + '_dictionary_website_button').clicked.connect(getattr(self, x + '_dictionary_website'))
def clear_search_history(self):
from calibre.gui2 import config
config['viewer_search_history'] = []
config['viewer_toc_search_history'] = []
def save_theme(self):
themename, ok = QInputDialog.getText(self, _('Theme name'),
_('Choose a name for this theme'))
if not ok:
return
themename = unicode_type(themename).strip()
if not themename:
return
c = config('')
c.add_opt('theme_name_xxx', default=themename)
self.save_options(c)
self.themes['theme_'+themename] = c.src
self.init_load_themes()
self.theming_message.setText(_('Saved settings as the theme named: %s')%
themename)
def init_load_themes(self):
for x in ('load', 'delete'):
m = getattr(self, '%s_theme_button'%x).menu()
m.clear()
for x in self.themes:
title = x[len('theme_'):]
ac = m.addAction(title)
ac.theme_id = x
def load_theme(self, ac):
theme = ac.theme_id
raw = self.themes[theme]
self.load_options(config(raw).parse())
self.theming_message.setText(_('Loaded settings from the theme %s')%
theme[len('theme_'):])
def delete_theme(self, ac):
theme = ac.theme_id
del self.themes[theme]
self.init_load_themes()
self.theming_message.setText(_('Deleted the theme named: %s')%
theme[len('theme_'):])
def init_dictionaries(self):
from calibre.gui2.viewer.main import dprefs
self.word_lookups = dprefs['word_lookups']
@property
def word_lookups(self):
return dict(self.dictionary_list.item(i).data(Qt.UserRole) for i in range(self.dictionary_list.count()))
@word_lookups.setter
def word_lookups(self, wl):
self.dictionary_list.clear()
for langcode, url in sorted(iteritems(wl), key=lambda lc_url:sort_key(calibre_langcode_to_name(lc_url[0]))):
i = QListWidgetItem('%s: %s' % (calibre_langcode_to_name(langcode), url), self.dictionary_list)
i.setData(Qt.UserRole, (langcode, url))
def add_dictionary_website(self):
class AD(QDialog):
def __init__(self, parent):
QDialog.__init__(self, parent)
self.setWindowTitle(_('Add a dictionary website'))
self.l = l = QFormLayout(self)
self.la = la = QLabel('<p>'+
_('Choose a language and enter the website address (URL) for it below.'
' The URL must have the placeholder <b>%s</b> in it, which will be replaced by the actual word being'
' looked up') % '{word}')
la.setWordWrap(True)
l.addRow(la)
self.le = LanguagesEdit(self)
l.addRow(_('&Language:'), self.le)
self.url = u = QLineEdit(self)
u.setMinimumWidth(350)
u.setPlaceholderText(_('For example: %s') % 'https://dictionary.com/{word}')
l.addRow(_('&URL:'), u)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
l.addRow(bb)
bb.accepted.connect(self.accept), bb.rejected.connect(self.reject)
self.resize(self.sizeHint())
def accept(self):
if '{word}' not in self.url.text():
return error_dialog(self, _('Invalid URL'), _(
'The URL {0} does not have the placeholder <b>{1}</b> in it.').format(self.url.text(), '{word}'), show=True)
QDialog.accept(self)
d = AD(self)
if d.exec_() == d.Accepted:
url = d.url.text()
if url:
wl = self.word_lookups
for lc in d.le.lang_codes:
wl[lc] = url
self.word_lookups = wl
def remove_dictionary_website(self):
idx = self.dictionary_list.currentIndex()
if idx.isValid():
lc, url = idx.data(Qt.UserRole)
wl = self.word_lookups
wl.pop(lc, None)
self.word_lookups = wl
def change_dictionary_website(self):
idx = self.dictionary_list.currentIndex()
if idx.isValid():
lc, url = idx.data(Qt.UserRole)
url, ok = QInputDialog.getText(self, _('Enter new website'), 'URL:', text=url)
if ok:
wl = self.word_lookups
wl[lc] = url
self.word_lookups = wl
def restore_defaults(self):
opts = config('').parse()
self.load_options(opts)
from calibre.gui2.viewer.main import dprefs, vprefs
self.word_lookups = dprefs.defaults['word_lookups']
self.opt_singleinstance.setChecked(vprefs.defaults['singleinstance'])
def load_options(self, opts):
self.opt_remember_window_size.setChecked(opts.remember_window_size)
self.opt_remember_current_page.setChecked(opts.remember_current_page)
self.opt_copy_bookmarks_to_file.setChecked(opts.copy_bookmarks_to_file)
self.opt_wheel_flips_pages.setChecked(opts.wheel_flips_pages)
self.opt_wheel_scroll_fraction.setValue(opts.wheel_scroll_fraction)
self.opt_line_scroll_fraction.setValue(opts.line_scroll_fraction)
self.opt_tap_flips_pages.setChecked(opts.tap_flips_pages)
self.opt_page_flip_duration.setValue(opts.page_flip_duration)
fms = opts.font_magnification_step
if fms < 0.01 or fms > 1:
fms = 0.2
self.opt_font_mag_step.setValue(int(fms*100))
self.opt_line_scrolling_stops_on_pagebreaks.setChecked(
opts.line_scrolling_stops_on_pagebreaks)
self.serif_family.setCurrentFont(QFont(opts.serif_family))
self.sans_family.setCurrentFont(QFont(opts.sans_family))
self.mono_family.setCurrentFont(QFont(opts.mono_family))
self.default_font_size.setValue(opts.default_font_size)
self.minimum_font_size.setValue(opts.minimum_font_size)
self.mono_font_size.setValue(opts.mono_font_size)
self.standard_font.setCurrentIndex(
{'serif':0, 'sans':1, 'mono':2}[opts.standard_font])
self.css.setPlainText(opts.user_css)
self.max_fs_width.setValue(opts.max_fs_width)
self.max_fs_height.setValue(opts.max_fs_height)
pats, names = self.hyphenate_pats, self.hyphenate_names
try:
idx = pats.index(opts.hyphenate_default_lang)
except ValueError:
idx = pats.index('en_us')
idx = self.hyphenate_default_lang.findText(names[idx])
self.hyphenate_default_lang.setCurrentIndex(idx)
self.hyphenate.setChecked(opts.hyphenate)
self.hyphenate_default_lang.setEnabled(opts.hyphenate)
self.search_online_url.setText(opts.search_online_url or '')
self.opt_fit_images.setChecked(opts.fit_images)
self.opt_fullscreen_clock.setChecked(opts.fullscreen_clock)
self.opt_fullscreen_scrollbar.setChecked(opts.fullscreen_scrollbar)
self.opt_start_in_fullscreen.setChecked(opts.start_in_fullscreen)
self.opt_show_fullscreen_help.setChecked(opts.show_fullscreen_help)
self.opt_fullscreen_pos.setChecked(opts.fullscreen_pos)
self.opt_cols_per_screen_portrait.setValue(opts.cols_per_screen_portrait)
self.opt_cols_per_screen_landscape.setValue(opts.cols_per_screen_landscape)
self.opt_override_book_margins.setChecked(not opts.use_book_margins)
for x in ('top', 'bottom', 'side'):
getattr(self, 'opt_%s_margin'%x).setValue(getattr(opts,
x+'_margin'))
for x in ('text', 'background'):
setattr(self, 'current_%s_color'%x, getattr(opts, '%s_color'%x))
self.update_sample_colors()
self.opt_show_controls.setChecked(opts.show_controls)
from calibre.gui2.viewer.main import vprefs
self.opt_singleinstance.setChecked(bool(vprefs['singleinstance']))
def change_color(self, which, reset=False):
if reset:
setattr(self, 'current_%s_color'%which, None)
else:
initial = getattr(self, 'current_%s_color'%which)
if initial:
initial = QColor(initial)
else:
initial = Qt.black if which == 'text' else Qt.white
title = (_('Choose text color') if which == 'text' else
_('Choose background color'))
col = QColorDialog.getColor(initial, self,
title, QColorDialog.ShowAlphaChannel)
if col.isValid():
name = unicode_type(col.name())
setattr(self, 'current_%s_color'%which, name)
self.update_sample_colors()
def update_sample_colors(self):
for x in ('text', 'background'):
val = getattr(self, 'current_%s_color'%x)
if not val:
val = 'inherit' if x == 'text' else 'transparent'
ss = 'QLabel { %s: %s }'%('background-color' if x == 'background'
else 'color', val)
getattr(self, '%s_color_sample'%x).setStyleSheet(ss)
def accept(self, *args):
if self.shortcut_config.is_editing:
from calibre.gui2 import info_dialog
info_dialog(self, _('Still editing'),
_('You are in the middle of editing a keyboard shortcut.'
' First complete that by clicking outside the'
' shortcut editing box.'), show=True)
return
self.save_options(config())
return QDialog.accept(self, *args)
def save_options(self, c):
c.set('serif_family', unicode_type(self.serif_family.currentFont().family()))
c.set('sans_family', unicode_type(self.sans_family.currentFont().family()))
c.set('mono_family', unicode_type(self.mono_family.currentFont().family()))
c.set('default_font_size', self.default_font_size.value())
c.set('minimum_font_size', self.minimum_font_size.value())
c.set('mono_font_size', self.mono_font_size.value())
c.set('standard_font', {0:'serif', 1:'sans', 2:'mono'}[
self.standard_font.currentIndex()])
c.set('user_css', unicode_type(self.css.toPlainText()))
c.set('remember_window_size', self.opt_remember_window_size.isChecked())
c.set('fit_images', self.opt_fit_images.isChecked())
c.set('max_fs_width', int(self.max_fs_width.value()))
max_fs_height = self.max_fs_height.value()
if max_fs_height <= self.max_fs_height.minimum():
max_fs_height = -1
c.set('max_fs_height', max_fs_height)
c.set('hyphenate', self.hyphenate.isChecked())
c.set('remember_current_page', self.opt_remember_current_page.isChecked())
c.set('copy_bookmarks_to_file', self.opt_copy_bookmarks_to_file.isChecked())
c.set('wheel_flips_pages', self.opt_wheel_flips_pages.isChecked())
c.set('wheel_scroll_fraction', self.opt_wheel_scroll_fraction.value())
c.set('line_scroll_fraction', self.opt_line_scroll_fraction.value())
c.set('tap_flips_pages', self.opt_tap_flips_pages.isChecked())
c.set('page_flip_duration', self.opt_page_flip_duration.value())
c.set('font_magnification_step',
float(self.opt_font_mag_step.value())/100.)
idx = self.hyphenate_default_lang.currentIndex()
c.set('hyphenate_default_lang',
self.hyphenate_default_lang.itemData(idx))
c.set('line_scrolling_stops_on_pagebreaks',
self.opt_line_scrolling_stops_on_pagebreaks.isChecked())
c.set('search_online_url', self.search_online_url.text().strip())
c.set('fullscreen_clock', self.opt_fullscreen_clock.isChecked())
c.set('fullscreen_pos', self.opt_fullscreen_pos.isChecked())
c.set('fullscreen_scrollbar', self.opt_fullscreen_scrollbar.isChecked())
c.set('show_fullscreen_help', self.opt_show_fullscreen_help.isChecked())
c.set('cols_per_screen_migrated', True)
c.set('cols_per_screen_portrait', int(self.opt_cols_per_screen_portrait.value()))
c.set('cols_per_screen_landscape', int(self.opt_cols_per_screen_landscape.value()))
c.set('start_in_fullscreen', self.opt_start_in_fullscreen.isChecked())
c.set('use_book_margins', not
self.opt_override_book_margins.isChecked())
c.set('text_color', self.current_text_color)
c.set('background_color', self.current_background_color)
c.set('show_controls', self.opt_show_controls.isChecked())
for x in ('top', 'bottom', 'side'):
c.set(x+'_margin', int(getattr(self, 'opt_%s_margin'%x).value()))
from calibre.gui2.viewer.main import dprefs, vprefs
dprefs['word_lookups'] = self.word_lookups
vprefs['singleinstance'] = self.opt_singleinstance.isChecked()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,187 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from __future__ import absolute_import, division, print_function, unicode_literals
import os
from PyQt5.Qt import QNetworkReply, QNetworkAccessManager, QUrl, QNetworkRequest, QTimer, pyqtSignal, QByteArray
from calibre import guess_type as _guess_type, prints
from calibre.constants import FAKE_HOST, FAKE_PROTOCOL, DEBUG
from calibre.ebooks.oeb.base import OEB_DOCS
from calibre.ebooks.oeb.display.webview import cleanup_html, load_as_html
from calibre.utils.short_uuid import uuid4
from polyglot.builtins import unicode_type
def guess_type(x):
return _guess_type(x)[0] or 'application/octet-stream'
cc_header = QByteArray(b'Cache-Control'), QByteArray(b'max-age=864001')
class NetworkReply(QNetworkReply):
def __init__(self, parent, request, mime_type, data):
QNetworkReply.__init__(self, parent)
self.setOpenMode(QNetworkReply.ReadOnly | QNetworkReply.Unbuffered)
self.setRequest(request)
self.setUrl(request.url())
self._aborted = False
self.__data = data
self.setHeader(QNetworkRequest.ContentTypeHeader, mime_type)
self.setHeader(QNetworkRequest.ContentLengthHeader, len(self.__data))
self.setRawHeader(*cc_header)
QTimer.singleShot(0, self.finalize_reply)
def bytesAvailable(self):
return len(self.__data)
def isSequential(self):
return True
def abort(self):
pass
def readData(self, maxlen):
if maxlen >= len(self.__data):
ans, self.__data = self.__data, b''
return ans
ans, self.__data = self.__data[:maxlen], self.__data[maxlen:]
return ans
read = readData
def finalize_reply(self):
self.setFinished(True)
self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200)
self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Ok")
self.metaDataChanged.emit()
self.downloadProgress.emit(len(self.__data), len(self.__data))
self.readyRead.emit()
self.finished.emit()
class NotFound(QNetworkReply):
def __init__(self, parent, request):
QNetworkReply.__init__(self, parent)
self.setOpenMode(QNetworkReply.ReadOnly | QNetworkReply.Unbuffered)
self.setHeader(QNetworkRequest.ContentTypeHeader, 'application/octet-stream')
self.setHeader(QNetworkRequest.ContentLengthHeader, 0)
self.setRequest(request)
self.setUrl(request.url())
QTimer.singleShot(0, self.finalize_reply)
def bytesAvailable(self):
return 0
def isSequential(self):
return True
def abort(self):
pass
def readData(self, maxlen):
return b''
def finalize_reply(self):
self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 404)
self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Not Found")
self.finished.emit()
def normpath(p):
return os.path.normcase(os.path.abspath(p))
class NetworkAccessManager(QNetworkAccessManager):
load_error = pyqtSignal(object, object)
def __init__(self, parent=None):
QNetworkAccessManager.__init__(self, parent)
self.mathjax_prefix = unicode_type(uuid4())
self.mathjax_base = '%s://%s/%s/' % (FAKE_PROTOCOL, FAKE_HOST, self.mathjax_prefix)
self.root = self.orig_root = os.path.dirname(P('viewer/blank.html', allow_user_override=False))
self.mime_map, self.single_pages, self.codec_map = {}, set(), {}
self.mathjax_dir = P('mathjax', allow_user_override=False)
def set_book_data(self, root, spine):
self.orig_root = root
self.root = normpath(root)
self.mime_map, self.single_pages, self.codec_map = {}, set(), {}
for p in spine:
mt = getattr(p, 'mime_type', None)
key = normpath(p)
if mt is not None:
self.mime_map[key] = mt
self.codec_map[key] = getattr(p, 'encoding', 'utf-8')
if getattr(p, 'is_single_page', False):
self.single_pages.add(key)
def is_single_page(self, path):
if not path:
return False
key = normpath(path)
return key in self.single_pages
def as_abspath(self, qurl):
name = qurl.path()[1:]
return os.path.join(self.orig_root, *name.split('/'))
def as_url(self, abspath):
name = os.path.relpath(abspath, self.root).replace('\\', '/')
ans = QUrl()
ans.setScheme(FAKE_PROTOCOL), ans.setAuthority(FAKE_HOST), ans.setPath('/' + name)
return ans
def guess_type(self, name):
mime_type = guess_type(name)
mime_type = {
# Prevent warning in console about mimetype of fonts
'application/vnd.ms-opentype':'application/x-font-ttf',
'application/x-font-truetype':'application/x-font-ttf',
'application/x-font-opentype':'application/x-font-ttf',
'application/x-font-otf':'application/x-font-ttf',
'application/font-sfnt': 'application/x-font-ttf',
}.get(mime_type, mime_type)
return mime_type
def preprocess_data(self, data, path):
mt = self.mime_map.get(path, self.guess_type(path))
if mt.lower() in OEB_DOCS:
enc = self.codec_map.get(path, 'utf-8')
html = data.decode(enc, 'replace')
html = cleanup_html(html)
data = html.encode('utf-8')
if load_as_html(html):
mt = 'text/html; charset=utf-8'
else:
mt = 'application/xhtml+xml; charset=utf-8'
return data, mt
def createRequest(self, operation, request, data):
qurl = request.url()
if operation == QNetworkAccessManager.GetOperation and qurl.host() == FAKE_HOST:
name = qurl.path()[1:]
if name.startswith(self.mathjax_prefix):
base = normpath(self.mathjax_dir)
path = normpath(os.path.join(base, name.partition('/')[2]))
else:
base = self.root
path = normpath(os.path.join(self.root, name))
if path.startswith(base) and os.path.exists(path):
try:
with lopen(path, 'rb') as f:
data = f.read()
data, mime_type = self.preprocess_data(data, path)
return NetworkReply(self, request, mime_type, data)
except Exception:
import traceback
self.load_error.emit(name, traceback.format_exc())
if DEBUG:
prints('URL not found in book: %r' % qurl.toString())
return NotFound(self, request)
return QNetworkAccessManager.createRequest(self, operation, request)

View File

@ -1,116 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt5.Qt import QWidget, QPainter, QPropertyAnimation, QEasingCurve, \
QRect, QPixmap, Qt, pyqtProperty
class SlideFlip(QWidget):
# API {{{
# In addition the isVisible() and setVisible() methods must be present
def __init__(self, parent):
QWidget.__init__(self, parent)
self.setGeometry(0, 0, 1, 1)
self._current_width = 0
self.before_image = self.after_image = None
self.animation = QPropertyAnimation(self, b'current_width', self)
self.setVisible(False)
self.animation.valueChanged.connect(self.update)
self.animation.finished.connect(self.finished)
self.flip_forwards = True
self.setAttribute(Qt.WA_OpaquePaintEvent)
@property
def running(self):
'True iff animation is currently running'
return self.animation.state() == self.animation.Running
def initialize(self, image, forwards=True):
'''
Initialize the flipper, causes the flipper to show itself displaying
the full `image`.
:param image: The image to display as background
:param forwards: If True flipper will flip forwards, otherwise
backwards
'''
self.flip_forwards = forwards
self.before_image = QPixmap.fromImage(image)
self.after_image = None
self.setGeometry(0, 0, image.width(), image.height())
self.setVisible(True)
def __call__(self, image, duration=0.5):
'''
Start the animation. You must have called :meth:`initialize` first.
:param duration: Animation duration in seconds.
'''
if self.running:
return
self.after_image = QPixmap.fromImage(image)
if self.flip_forwards:
self.animation.setStartValue(image.width())
self.animation.setEndValue(0)
t = self.before_image
self.before_image = self.after_image
self.after_image = t
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.InExpo))
else:
self.animation.setStartValue(0)
self.animation.setEndValue(image.width())
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
self.animation.setDuration(duration * 1000)
self.animation.start()
# }}}
def finished(self):
self.setVisible(False)
self.before_image = self.after_image = None
def paintEvent(self, ev):
if self.before_image is None:
return
canvas_size = self.rect()
p = QPainter(self)
p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
p.drawPixmap(canvas_size, self.before_image,
self.before_image.rect())
if self.after_image is not None:
width = self._current_width
iw = self.after_image.width()
sh = min(self.after_image.height(), canvas_size.height())
if self.flip_forwards:
source = QRect(max(0, iw - width), 0, width, sh)
else:
source = QRect(0, 0, width, sh)
target = QRect(source)
target.moveLeft(0)
p.drawPixmap(target, self.after_image, source)
p.end()
def set_current_width(self, val):
self._current_width = val
current_width = pyqtProperty('int',
fget=lambda self: self._current_width,
fset=set_current_width
)

View File

@ -1,155 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import json
from collections import defaultdict
from PyQt5.Qt import (
QUrl, QWidget, QHBoxLayout, QSize, pyqtSlot, QVBoxLayout, QToolButton,
QIcon, pyqtSignal)
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
from PyQt5.QtWebKit import QWebSettings
from calibre import prints
from calibre.constants import DEBUG, FAKE_PROTOCOL, FAKE_HOST
from calibre.ebooks.oeb.display.webview import load_html
from polyglot.builtins import unicode_type
class FootnotesPage(QWebPage):
def __init__(self, parent):
QWebPage.__init__(self, parent)
self.js_loader = None
self._footnote_data = ''
from calibre.gui2.viewer.documentview import apply_basic_settings
settings = self.settings()
apply_basic_settings(settings)
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, False)
self.setLinkDelegationPolicy(self.DelegateAllLinks)
self.mainFrame().javaScriptWindowObjectCleared.connect(self.add_window_objects)
self.add_window_objects()
def add_window_objects(self, add_ready_listener=True):
self.mainFrame().addToJavaScriptWindowObject("py_bridge", self)
evaljs = self.mainFrame().evaluateJavaScript
if self.js_loader is not None:
for x in 'utils extract'.split():
evaljs(self.js_loader.get(x))
@pyqtSlot(str)
def debug(self, msg):
prints(msg)
@pyqtSlot(result=str)
def footnote_data(self):
return self._footnote_data
def set_footnote_data(self, target, known_targets):
self._footnote_data = json.dumps({'target':target, 'known_targets':known_targets})
if self._footnote_data:
self.mainFrame().evaluateJavaScript(
'data = JSON.parse(py_bridge.footnote_data()); calibre_extract.show_footnote(data["target"], data["known_targets"])')
def javaScriptAlert(self, frame, msg):
prints('FootnoteView:alert::', msg)
def javaScriptConsoleMessage(self, msg, lineno, source_id):
if DEBUG:
prints('FootnoteView:%s:%s:'%(unicode_type(source_id), lineno), unicode_type(msg))
class FootnotesView(QWidget):
follow_link = pyqtSignal()
close_view = pyqtSignal()
def __init__(self, parent):
QWidget.__init__(self, parent)
self.l = l = QHBoxLayout(self)
self.vl = vl = QVBoxLayout()
self.view = v = QWebView(self)
self._page = FootnotesPage(v)
v.setPage(self._page)
l.addWidget(v), l.addLayout(vl)
self.goto_button = b = QToolButton(self)
b.setIcon(QIcon(I('forward.png'))), b.setToolTip(_('Go to this footnote in the main view'))
b.clicked.connect(self.follow_link)
vl.addWidget(b)
self.close_button = b = QToolButton(self)
b.setIcon(QIcon(I('window-close.png'))), b.setToolTip(_('Close the footnotes window'))
b.clicked.connect(self.close_view)
vl.addWidget(b)
def page(self):
return self._page
def sizeHint(self):
return QSize(400, 200)
class Footnotes(object):
def __init__(self, view):
self.view = view
self.clear()
def set_footnotes_view(self, fv):
self.footnotes_view = fv
self.clone_settings()
fv.page().linkClicked.connect(self.view.footnote_link_clicked)
fv.page().js_loader = self.view.document.js_loader
def clone_settings(self):
source = self.view.document.settings()
settings = self.footnotes_view.page().settings()
for x in 'DefaultFontSize DefaultFixedFontSize MinimumLogicalFontSize MinimumFontSize StandardFont SerifFont SansSerifFont FixedFont'.split():
func = 'setFontSize' if x.endswith('FontSize') else 'setFontFamily'
getattr(settings, func)(getattr(QWebSettings, x), getattr(source, 'f' + func[4:])(getattr(QWebSettings, x)))
settings.setUserStyleSheetUrl(source.userStyleSheetUrl())
def clear(self):
self.known_footnote_targets = defaultdict(set)
self.showing_url = None
def spine_path(self, path):
try:
si = self.view.manager.iterator.spine.index(path)
return self.view.manager.iterator.spine[si]
except (AttributeError, ValueError):
pass
def get_footnote_data(self, a, qurl):
current_path = self.view.path()
if not current_path:
return # Not viewing a local file
dest_path = self.spine_path(self.view.path(qurl))
if dest_path is not None:
if dest_path == current_path:
# We deliberately ignore linked to anchors if the destination is
# the same as the source, because many books have section ToCs
# that are linked back from their destinations, for example,
# the calibre User Manual
linked_to_anchors = {}
else:
linked_to_anchors = {anchor:0 for path, anchor in dest_path.verified_links if path == current_path}
if a.evaluateJavaScript('calibre_extract.is_footnote_link(this, "%s://%s", %s)' % (FAKE_PROTOCOL, FAKE_HOST, json.dumps(linked_to_anchors))):
if dest_path not in self.known_footnote_targets:
self.known_footnote_targets[dest_path] = s = set()
for item in self.view.manager.iterator.spine:
for path, target in item.verified_links:
if target and path == dest_path:
s.add(target)
return (dest_path, qurl.fragment(QUrl.FullyDecoded), qurl)
def show_footnote(self, fd):
path, target, self.showing_url = fd
if hasattr(self, 'footnotes_view'):
if load_html(path, self.footnotes_view.view, codec=getattr(path, 'encoding', 'utf-8'),
mime_type=getattr(path, 'mime_type', 'text/html')):
self.footnotes_view.page().set_footnote_data(target, {k:True for k in self.known_footnote_targets[path]})

View File

@ -1,377 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import time, sys
from functools import partial
from PyQt5.Qt import (
QObject, QPointF, pyqtSignal, QEvent, QApplication, QMouseEvent, Qt,
QContextMenuEvent, QDialog, QDialogButtonBox, QLabel, QVBoxLayout)
from calibre.constants import iswindows
from polyglot.builtins import itervalues, map
touch_supported = False
if iswindows and sys.getwindowsversion()[:2] >= (6, 2): # At least windows 7
touch_supported = True
SWIPE_HOLD_INTERVAL = 0.5 # seconds
HOLD_THRESHOLD = 1.0 # seconds
TAP_THRESHOLD = 50 # manhattan pixels
SWIPE_DISTANCE = 100 # manhattan pixels
PINCH_CENTER_THRESHOLD = 150 # manhattan pixels
PINCH_SQUEEZE_FACTOR = 2.5 # smaller length must be less that larger length / squeeze factor
Tap, TapAndHold, Pinch, Swipe, SwipeAndHold = 'Tap', 'TapAndHold', 'Pinch', 'Swipe', 'SwipeAndHold'
Left, Right, Up, Down = 'Left', 'Right', 'Up', 'Down'
In, Out = 'In', 'Out'
class Help(QDialog): # {{{
def __init__(self, parent=None):
QDialog.__init__(self, parent=parent)
self.l = l = QVBoxLayout(self)
self.setLayout(l)
self.la = la = QLabel(
'''
<style>
h2 { text-align: center }
dt { font-weight: bold }
dd { margin-bottom: 1.5em }
</style>
''' + _(
'''
<h2>The list of available gestures</h2>
<dl>
<dt>Single tap</dt>
<dd>A single tap on the right two thirds of the page will turn to the next page
and on the left one-third of the page will turn to the previous page. Single tapping
on a link will activate the link.</dd>
<dt>Swipe</dt>
<dd>Swipe to the left to go to the next page and to the right to go to the previous page.
This mimics turning pages in a paper book. When the viewer is not in paged mode, swiping
scrolls the text line by line instead of page by page.</dd>
<dt>Pinch</dt>
<dd>Pinch in or out to decrease or increase the font size</dd>
<dt>Swipe and hold</dt>
<dd>If you swipe and the hold your finger down instead of lifting it, pages will be turned
rapidly allowing for quickly scanning through large numbers of pages.</dd>
<dt>Tap and hold</dt>
<dd>Bring up the context (right-click) menu</dd>
</dl>
'''
))
la.setAlignment(Qt.AlignTop | Qt.AlignLeft)
la.setWordWrap(True)
l.addWidget(la, Qt.AlignTop|Qt.AlignLeft)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Close)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
l.addWidget(bb)
self.resize(600, 500)
# }}}
class TouchPoint(object):
def __init__(self, tp):
self.creation_time = self.last_update_time = self.time_of_last_move = time.time()
self.start_screen_position = self.current_screen_position = self.previous_screen_position = QPointF(tp.screenPos())
self.time_since_last_update = -1
self.total_movement = 0
def update(self, tp):
now = time.time()
self.time_since_last_update = now - self.last_update_time
self.last_update_time = now
self.previous_screen_position, self.current_screen_position = self.current_screen_position, QPointF(tp.screenPos())
movement = (self.current_screen_position - self.previous_screen_position).manhattanLength()
self.total_movement += movement
if movement > 5:
self.time_of_last_move = now
@property
def swipe_type(self):
x_movement = self.current_screen_position.x() - self.start_screen_position.x()
y_movement = self.current_screen_position.y() - self.start_screen_position.y()
xabs, yabs = map(abs, (x_movement, y_movement))
if max(xabs, yabs) < SWIPE_DISTANCE or min(xabs/max(yabs, 0.01), yabs/max(xabs, 0.01)) > 0.3:
return
d = x_movement if xabs > yabs else y_movement
axis = (Left, Right) if xabs > yabs else (Up, Down)
return axis[0 if d < 0 else 1]
@property
def swipe_live(self):
x_movement = self.current_screen_position.x() - self.previous_screen_position.x()
y_movement = self.current_screen_position.y() - self.previous_screen_position.y()
return (x_movement, y_movement)
def get_pinch(p1, p2):
starts = [p1.start_screen_position, p2.start_screen_position]
ends = [p1.current_screen_position, p2.current_screen_position]
start_center = (starts[0] + starts[1]) / 2.0
end_center = (ends[0] + ends[1]) / 2.0
if (start_center - end_center).manhattanLength() > PINCH_CENTER_THRESHOLD:
return None
start_length = (starts[0] - starts[1]).manhattanLength()
end_length = (ends[0] - ends[1]).manhattanLength()
if min(start_length, end_length) > max(start_length, end_length) / PINCH_SQUEEZE_FACTOR:
return None
return In if start_length > end_length else Out
class State(QObject):
tapped = pyqtSignal(object)
swiped = pyqtSignal(object)
swiping = pyqtSignal(object, object)
pinched = pyqtSignal(object)
tap_hold_started = pyqtSignal(object)
tap_hold_updated = pyqtSignal(object)
swipe_hold_started = pyqtSignal(object)
swipe_hold_updated = pyqtSignal(object)
tap_hold_finished = pyqtSignal(object)
swipe_hold_finished = pyqtSignal(object)
def __init__(self):
QObject.__init__(self)
self.clear()
def clear(self):
self.possible_gestures = set()
self.touch_points = {}
self.hold_started = False
self.hold_data = None
def start(self):
self.clear()
self.possible_gestures = {Tap, TapAndHold, Swipe, Pinch, SwipeAndHold}
def update(self, ev, boundary='update'):
if boundary == 'start':
self.start()
for tp in ev.touchPoints():
tpid = tp.id()
if tpid not in self.touch_points:
self.touch_points[tpid] = TouchPoint(tp)
else:
self.touch_points[tpid].update(tp)
if len(self.touch_points) > 2:
self.possible_gestures.clear()
elif len(self.touch_points) > 1:
self.possible_gestures &= {Pinch}
if boundary == 'end':
self.check_for_holds()
self.finalize()
self.clear()
else:
self.check_for_holds()
if {Swipe, SwipeAndHold} & self.possible_gestures:
tp = next(itervalues(self.touch_points))
self.swiping.emit(*tp.swipe_live)
def check_for_holds(self):
if not {SwipeAndHold, TapAndHold} & self.possible_gestures:
return
now = time.time()
tp = next(itervalues(self.touch_points))
if now - tp.time_of_last_move < HOLD_THRESHOLD:
return
if self.hold_started:
if TapAndHold in self.possible_gestures:
self.tap_hold_updated.emit(tp)
if SwipeAndHold in self.possible_gestures:
self.swipe_hold_updated.emit(self.hold_data[1])
else:
self.possible_gestures &= {TapAndHold, SwipeAndHold}
if tp.total_movement > TAP_THRESHOLD:
st = tp.swipe_type
if st is None:
self.possible_gestures.clear()
else:
self.hold_started = True
self.possible_gestures = {SwipeAndHold}
self.hold_data = (now, st)
self.swipe_hold_started.emit(st)
else:
self.possible_gestures = {TapAndHold}
self.hold_started = True
self.hold_data = now
self.tap_hold_started.emit(tp)
def finalize(self):
if Tap in self.possible_gestures:
tp = next(itervalues(self.touch_points))
if tp.total_movement <= TAP_THRESHOLD:
self.tapped.emit(tp)
return
if Swipe in self.possible_gestures:
tp = next(itervalues(self.touch_points))
st = tp.swipe_type
if st is not None:
self.swiped.emit(st)
return
if Pinch in self.possible_gestures:
points = tuple(itervalues(self.touch_points))
if len(points) == 2:
pinch_dir = get_pinch(*points)
if pinch_dir is not None:
self.pinched.emit(pinch_dir)
if not self.hold_started:
return
if TapAndHold in self.possible_gestures:
tp = next(itervalues(self.touch_points))
self.tap_hold_finished.emit(tp)
return
if SwipeAndHold in self.possible_gestures:
self.swipe_hold_finished.emit(self.hold_data[1])
return
class GestureHandler(QObject):
def __init__(self, view):
QObject.__init__(self, view)
self.state = State()
self.last_swipe_hold_update = None
self.state.swiped.connect(self.handle_swipe)
self.state.tapped.connect(self.handle_tap)
self.state.swiping.connect(self.handle_swiping)
self.state.tap_hold_started.connect(partial(self.handle_tap_hold, 'start'))
self.state.tap_hold_updated.connect(partial(self.handle_tap_hold, 'update'))
self.state.tap_hold_finished.connect(partial(self.handle_tap_hold, 'end'))
self.state.swipe_hold_started.connect(partial(self.handle_swipe_hold, 'start'))
self.state.swipe_hold_updated.connect(partial(self.handle_swipe_hold, 'update'))
self.state.swipe_hold_finished.connect(partial(self.handle_swipe_hold, 'end'))
self.state.pinched.connect(self.handle_pinch)
self.evmap = {QEvent.TouchBegin: 'start', QEvent.TouchUpdate: 'update', QEvent.TouchEnd: 'end'}
def __call__(self, ev):
if not touch_supported:
return False
etype = ev.type()
if etype in (
QEvent.MouseMove, QEvent.MouseButtonPress,
QEvent.MouseButtonRelease, QEvent.MouseButtonDblClick) and ev.source() != Qt.MouseEventNotSynthesized:
# swallow fake mouse events generated from touch events
ev.accept()
return True
boundary = self.evmap.get(etype, None)
if boundary is None:
return False
self.state.update(ev, boundary=boundary)
ev.accept()
return True
def close_open_menu(self):
m = getattr(self.parent(), 'context_menu', None)
if m is not None and m.isVisible():
m.close()
return True
def handle_swipe(self, direction):
if self.close_open_menu():
return
view = self.parent()
if not view.document.in_paged_mode:
return
func = {Left:'next_page', Right: 'previous_page', Down:'goto_previous_section', Up:'goto_next_section'}[direction]
getattr(view, func)()
def handle_swiping(self, x, y):
if max(abs(x), abs(y)) < 1:
return
view = self.parent()
if view.document.in_paged_mode:
return
ydirection = Up if y < 0 else Down
if view.manager is not None and abs(y) > 0:
if ydirection is Up and view.document.at_bottom:
view.manager.next_document()
return
elif ydirection is Down and view.document.at_top:
view.manager.previous_document()
return
view.scroll_by(x=-x, y=-y)
if view.manager is not None:
view.manager.scrolled(view.scroll_fraction)
def current_position(self, tp):
return self.parent().mapFromGlobal(tp.current_screen_position.toPoint())
def handle_tap(self, tp):
if self.close_open_menu():
return
view = self.parent()
mf = view.document.mainFrame()
r = mf.hitTestContent(self.current_position(tp))
if r.linkElement().isNull():
if view.document.tap_flips_pages:
threshold = view.width() / 3.0
attr = 'previous' if self.current_position(tp).x() <= threshold else 'next'
getattr(view, '%s_page'%attr)()
else:
for etype in (QEvent.MouseButtonPress, QEvent.MouseButtonRelease):
ev = QMouseEvent(etype, self.current_position(tp), tp.current_screen_position.toPoint(), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
QApplication.sendEvent(view, ev)
def handle_tap_hold(self, action, tp):
etype = {'start':QEvent.MouseButtonPress, 'update':QEvent.MouseMove, 'end':QEvent.MouseButtonRelease}[action]
ev = QMouseEvent(etype, self.current_position(tp), tp.current_screen_position.toPoint(), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
QApplication.sendEvent(self.parent(), ev)
if action == 'end':
ev = QContextMenuEvent(QContextMenuEvent.Other, self.current_position(tp), tp.current_screen_position.toPoint())
# We have to use post event otherwise the popup remains an alien widget and does not receive events
QApplication.postEvent(self.parent(), ev)
def handle_swipe_hold(self, action, direction):
view = self.parent()
if not view.document.in_paged_mode:
return
if action == 'start':
self.last_swipe_hold_update = time.time()
try:
self.handle_swipe(direction)
finally:
view.is_auto_repeat_event = False
elif action == 'update' and self.last_swipe_hold_update is not None and time.time() - self.last_swipe_hold_update > SWIPE_HOLD_INTERVAL:
view.is_auto_repeat_event = True
self.last_swipe_hold_update = time.time()
try:
self.handle_swipe(direction)
finally:
view.is_auto_repeat_event = False
elif action == 'end':
self.last_swipe_hold_update = None
def handle_pinch(self, direction):
attr = 'magnify' if direction is Out else 'shrink'
getattr(self.parent(), '%s_fonts' % attr)()
def show_help(self):
Help(self.parent()).exec_()
if __name__ == '__main__':
app = QApplication([])
Help().exec_()

View File

@ -1,177 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt5.Qt import (QDialog, QPixmap, QUrl, QScrollArea, QLabel, QSizePolicy,
QDialogButtonBox, QVBoxLayout, QPalette, QApplication, QSize, QIcon,
Qt, QTransform, QSvgRenderer, QImage, QPainter)
from calibre.gui2 import choose_save_file, gprefs, NO_URL_FORMATTING, max_available_height
from polyglot.builtins import unicode_type
def render_svg(widget, path):
img = QPixmap()
rend = QSvgRenderer()
if rend.load(path):
dpr = getattr(widget, 'devicePixelRatioF', widget.devicePixelRatio)()
sz = rend.defaultSize()
h = (max_available_height() - 50)
w = int(h * sz.height() / float(sz.width()))
pd = QImage(w * dpr, h * dpr, QImage.Format_RGB32)
pd.fill(Qt.white)
p = QPainter(pd)
rend.render(p)
p.end()
img = QPixmap.fromImage(pd)
img.setDevicePixelRatio(dpr)
return img
class ImageView(QDialog):
def __init__(self, parent, current_img, current_url, geom_name='viewer_image_popup_geometry'):
QDialog.__init__(self)
dw = QApplication.instance().desktop()
self.avail_geom = dw.availableGeometry(parent if parent is not None else self)
self.current_img = current_img
self.current_url = current_url
self.factor = 1.0
self.geom_name = geom_name
self.label = l = QLabel()
l.setBackgroundRole(QPalette.Base)
l.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
l.setScaledContents(True)
self.scrollarea = sa = QScrollArea()
sa.setBackgroundRole(QPalette.Dark)
sa.setWidget(l)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Close)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
self.zi_button = zi = bb.addButton(_('Zoom &in'), bb.ActionRole)
self.zo_button = zo = bb.addButton(_('Zoom &out'), bb.ActionRole)
self.save_button = so = bb.addButton(_('&Save as'), bb.ActionRole)
self.rotate_button = ro = bb.addButton(_('&Rotate'), bb.ActionRole)
zi.setIcon(QIcon(I('plus.png')))
zo.setIcon(QIcon(I('minus.png')))
so.setIcon(QIcon(I('save.png')))
ro.setIcon(QIcon(I('rotate-right.png')))
zi.clicked.connect(self.zoom_in)
zo.clicked.connect(self.zoom_out)
so.clicked.connect(self.save_image)
ro.clicked.connect(self.rotate_image)
self.l = l = QVBoxLayout()
self.setLayout(l)
l.addWidget(sa)
l.addWidget(bb)
def zoom_in(self):
self.factor *= 1.25
self.adjust_image(1.25)
def zoom_out(self):
self.factor *= 0.8
self.adjust_image(0.8)
def save_image(self):
filters=[('Images', ['png', 'jpeg', 'jpg'])]
f = choose_save_file(self, 'viewer image view save dialog',
_('Choose a file to save to'), filters=filters,
all_files=False)
if f:
from calibre.utils.img import save_image
save_image(self.current_img.toImage(), f)
def adjust_image(self, factor):
self.label.resize(self.factor * self.current_img.size())
self.zi_button.setEnabled(self.factor <= 3)
self.zo_button.setEnabled(self.factor >= 0.3333)
self.adjust_scrollbars(factor)
def adjust_scrollbars(self, factor):
for sb in (self.scrollarea.horizontalScrollBar(),
self.scrollarea.verticalScrollBar()):
sb.setValue(int(factor*sb.value()) + ((factor - 1) * sb.pageStep()/2))
def rotate_image(self):
pm = self.label.pixmap()
t = QTransform()
t.rotate(90)
pm = self.current_img = pm.transformed(t)
self.label.setPixmap(pm)
self.label.adjustSize()
self.factor = 1
for sb in (self.scrollarea.horizontalScrollBar(),
self.scrollarea.verticalScrollBar()):
sb.setValue(0)
def __call__(self, use_exec=False):
geom = self.avail_geom
self.label.setPixmap(self.current_img)
self.label.adjustSize()
self.resize(QSize(int(geom.width()/2.5), geom.height()-50))
geom = gprefs.get(self.geom_name, None)
if geom is not None:
self.restoreGeometry(geom)
try:
self.current_image_name = unicode_type(self.current_url.toString(NO_URL_FORMATTING)).rpartition('/')[-1]
except AttributeError:
self.current_image_name = self.current_url
title = _('View image: %s')%self.current_image_name
self.setWindowTitle(title)
if use_exec:
self.exec_()
else:
self.show()
def done(self, e):
gprefs[self.geom_name] = bytearray(self.saveGeometry())
return QDialog.done(self, e)
def wheelEvent(self, event):
d = event.angleDelta().y()
if abs(d) > 0 and not self.scrollarea.verticalScrollBar().isVisible():
event.accept()
(self.zoom_out if d < 0 else self.zoom_in)()
class ImagePopup(object):
def __init__(self, parent):
self.current_img = QPixmap()
self.current_url = QUrl()
self.parent = parent
self.dialogs = []
def __call__(self):
if self.current_img.isNull():
return
d = ImageView(self.parent, self.current_img, self.current_url)
self.dialogs.append(d)
d.finished.connect(self.cleanup, type=Qt.QueuedConnection)
d()
def cleanup(self):
for d in tuple(self.dialogs):
if not d.isVisible():
self.dialogs.remove(d)
if __name__ == '__main__':
import sys
from calibre.gui2 import Application
app = Application([])
p = QPixmap()
p.load(sys.argv[-1])
u = QUrl.fromLocalFile(sys.argv[-1])
d = ImageView(None, p, u)
d()
app.exec_()

View File

@ -1,53 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt5.Qt import QDialog, QDialogButtonBox, QVBoxLayout, QIcon, Qt
from PyQt5.QtWebKitWidgets import QWebInspector
from calibre.gui2 import gprefs
class WebInspector(QDialog):
def __init__(self, parent, page):
QDialog.__init__(self, parent)
self.setWindowFlags(self.windowFlags() | Qt.WindowMinMaxButtonsHint)
self.setWindowTitle(_('Inspect book code'))
self.setWindowIcon(QIcon(I('debug.png')))
l = QVBoxLayout()
self.setLayout(l)
self.inspector = QWebInspector(self)
self.inspector.setPage(page)
l.addWidget(self.inspector)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Close)
l.addWidget(bb)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
self.resize(self.sizeHint())
geom = gprefs.get('viewer_inspector_geom', None)
if geom is not None:
self.restoreGeometry(geom)
def save_geometry(self):
gprefs['viewer_inspector_geom'] = bytearray(self.saveGeometry())
def closeEvent(self, ev):
self.save_geometry()
return QDialog.closeEvent(self, ev)
def accept(self):
self.save_geometry()
QDialog.accept(self)
def reject(self):
self.save_geometry()
QDialog.reject(self)

View File

@ -1,82 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
import calibre
from calibre.utils.resources import compiled_coffeescript, load_hyphenator_dicts
from polyglot.builtins import iteritems
class JavaScriptLoader(object):
JS = {x:('viewer/%s.js'%x if y is None else y) for x, y in iteritems({
'bookmarks':None,
'referencing':None,
'hyphenation':None,
'jquery':'viewer/jquery.js',
'jquery_scrollTo':None,
'hyphenator':'viewer/hyphenate/Hyphenator.js',
'images':None
})}
CS = {
'cfi':'ebooks.oeb.display.cfi',
'indexing':'ebooks.oeb.display.indexing',
'paged':'ebooks.oeb.display.paged',
'utils':'ebooks.oeb.display.utils',
'fs':'ebooks.oeb.display.full_screen',
'math': 'ebooks.oeb.display.mathjax',
'extract': 'ebooks.oeb.display.extract',
}
ORDER = ('jquery', 'jquery_scrollTo', 'bookmarks', 'referencing', 'images',
'hyphenation', 'hyphenator', 'utils', 'cfi', 'indexing', 'paged',
'fs', 'math', 'extract')
def __init__(self, dynamic_coffeescript=False):
self._dynamic_coffeescript = dynamic_coffeescript
if self._dynamic_coffeescript:
try:
from calibre.utils.serve_coffee import compile_coffeescript
compile_coffeescript
except:
self._dynamic_coffeescript = False
print('WARNING: Failed to load serve_coffee, not compiling '
'coffeescript dynamically.')
self._cache = {}
self._hp_cache = {}
def get(self, name):
ans = self._cache.get(name, None)
if ans is None:
src = self.CS.get(name, None)
if src is None:
src = self.JS.get(name, None)
if src is None:
raise KeyError('No such resource: %s'%name)
ans = P(src, data=True,
allow_user_override=False).decode('utf-8')
else:
dynamic = self._dynamic_coffeescript and calibre.__file__ and not calibre.__file__.endswith('.pyo') and os.path.exists(calibre.__file__)
ans = compiled_coffeescript(src, dynamic=dynamic).decode('utf-8')
self._cache[name] = ans
return ans
def __call__(self, evaljs, lang, default_lang):
for x in self.ORDER:
src = self.get(x)
evaljs(src)
js, lang = load_hyphenator_dicts(self._hp_cache, lang, default_lang)
evaljs(js)
return lang

View File

@ -1,104 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.constants import isosx
SHORTCUTS = {
'Next Page' : (['PgDown', 'Space'],
_('Scroll to the next page')),
'Previous Page' : (['PgUp', 'Backspace', 'Shift+Space'],
_('Scroll to the previous page')),
'Next Section' : (['Ctrl+PgDown', 'Ctrl+Down'],
_('Scroll to the next section')),
'Previous Section' : (['Ctrl+PgUp', 'Ctrl+Up'],
_('Scroll to the previous section')),
'Section Bottom' : (['End'],
_('Scroll to the bottom of the section')),
'Section Top' : (['Home'],
_('Scroll to the top of the section')),
'Document Bottom' : (['Ctrl+End'],
_('Scroll to the end of the document')),
'Document Top' : (['Ctrl+Home'],
_('Scroll to the start of the document')),
'Down' : (['J', 'Down'],
_('Scroll down')),
'Up' : (['K', 'Up'],
_('Scroll up')),
'Left' : (['H', 'Left'],
_('Scroll left')),
'Right' : (['L', 'Right'],
_('Scroll right')),
'Back': (['Alt+Left'],
_('Back')),
'Forward': (['Alt+Right'],
_('Forward')),
'Quit': (['Ctrl+Q', 'Ctrl+W', 'Alt+F4'],
_('Quit')),
'Focus Search': (['/', 'Ctrl+F'],
_('Start search')),
'Show metadata': (['Ctrl+I'],
_('Show metadata')),
'Font larger': (['Ctrl+='],
_('Font size larger')),
'Font smaller': (['Ctrl+-'],
_('Font size smaller')),
'Fullscreen': ((['Ctrl+Meta+F'] if isosx else ['Ctrl+Shift+F', 'F11']),
_('Fullscreen')),
'Find next': (['F3'],
_('Find next')),
'Find previous': (['Shift+F3'],
_('Find previous')),
'Search online': (['Ctrl+E'],
_('Search online for word')),
'Table of Contents': (['Ctrl+T'],
_('Show/hide the Table of Contents')),
'Lookup word': (['Ctrl+L'],
_('Lookup word in dictionary')),
'Next occurrence': (['Ctrl+S'],
_('Go to next occurrence of selected word')),
'Bookmark': (['Ctrl+B'],
_('Bookmark the current location')),
'Toggle bookmarks': (['Ctrl+Alt+B'],
_('Show/hide bookmarks')),
'Reload': (['Ctrl+R', 'F5'],
_('Reload the current book')),
'Print': (['Ctrl+P'],
_('Print the current book')),
'Show/hide controls': (['Ctrl+F11'],
_('Show/hide controls')),
}

File diff suppressed because it is too large Load Diff

View File

@ -1,91 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import json, time, numbers
from PyQt5.Qt import QApplication, QEventLoop
from calibre.constants import DEBUG
class PagePosition(object):
def __init__(self, document):
self.document = document
document.jump_to_cfi_listeners.add(self)
self.cfi_job_id = 0
self.pending_scrolls = set()
@property
def viewport_cfi(self):
ans = self.document.mainFrame().evaluateJavaScript('''
ans = 'undefined';
if (window.paged_display) {
ans = window.paged_display.current_cfi();
if (!ans) ans = 'undefined';
}
ans;
''')
if ans in {'', 'undefined'}:
ans = None
return ans
def scroll_to_cfi(self, cfi):
if cfi:
jid = self.cfi_job_id
self.cfi_job_id += 1
cfi = json.dumps(cfi)
self.pending_scrolls.add(jid)
self.document.mainFrame().evaluateJavaScript(
'paged_display.jump_to_cfi(%s, %d)' % (cfi, jid))
# jump_to_cfi is async, so we wait for it to complete
st = time.time()
WAIT = 1 # seconds
while jid in self.pending_scrolls and time.time() - st < WAIT:
QApplication.processEvents(QEventLoop.ExcludeUserInputEvents | QEventLoop.ExcludeSocketNotifiers)
time.sleep(0.01)
if jid in self.pending_scrolls:
self.pending_scrolls.discard(jid)
if DEBUG:
print('jump_to_cfi() failed to complete after %s seconds' % WAIT)
@property
def current_pos(self):
ans = self.viewport_cfi
if not ans:
ans = self.document.scroll_fraction
return ans
def __enter__(self):
self.save()
def __exit__(self, *args):
self.restore()
def __call__(self, cfi_job_id):
self.pending_scrolls.discard(cfi_job_id)
def save(self, overwrite=True):
if not overwrite and self._cpos is not None:
return
self._cpos = self.current_pos
def restore(self):
if self._cpos is None:
return
self.to_pos(self._cpos)
self._cpos = None
def to_pos(self, pos):
if isinstance(pos, numbers.Number):
self.document.scroll_fraction = pos
else:
self.scroll_to_cfi(pos)
def set_pos(self, pos):
self._cpos = pos

View File

@ -1,230 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import os, subprocess, sys
from threading import Thread
from PyQt5.Qt import (
QFormLayout, QLineEdit, QToolButton, QHBoxLayout, QLabel, QIcon, QPrinter,
QPageSize, QComboBox, QDoubleSpinBox, QCheckBox, QProgressDialog, QTimer)
from calibre import sanitize_file_name
from calibre.ptempfile import PersistentTemporaryFile
from calibre.ebooks.conversion.plugins.pdf_output import PAPER_SIZES
from calibre.gui2 import elided_text, error_dialog, choose_save_file, Application, open_local_file, dynamic
from calibre.gui2.widgets2 import Dialog
from calibre.gui2.viewer.main import vprefs
from calibre.utils.icu import numeric_sort_key
from calibre.utils.ipc.simple_worker import start_pipe_worker
from calibre.utils.serialize import msgpack_dumps, msgpack_loads
class PrintDialog(Dialog):
OUTPUT_NAME = 'print-to-pdf-choose-file'
def __init__(self, book_title, parent=None, prefs=vprefs):
self.book_title = book_title
self.default_file_name = sanitize_file_name(book_title[:75] + '.pdf')
self.paper_size_map = {a:getattr(QPageSize, a.capitalize()) for a in PAPER_SIZES}
Dialog.__init__(self, _('Print to PDF'), 'print-to-pdf', prefs=prefs, parent=parent)
def setup_ui(self):
self.l = l = QFormLayout(self)
l.addRow(QLabel(_('Print %s to a PDF file') % elided_text(self.book_title)))
self.h = h = QHBoxLayout()
self.file_name = f = QLineEdit(self)
val = dynamic.get(self.OUTPUT_NAME, None)
if not val:
val = os.path.expanduser('~')
else:
val = os.path.dirname(val)
f.setText(os.path.abspath(os.path.join(val, self.default_file_name)))
self.browse_button = b = QToolButton(self)
b.setIcon(QIcon(I('document_open.png'))), b.setToolTip(_('Choose location for PDF file'))
b.clicked.connect(self.choose_file)
h.addWidget(f), h.addWidget(b)
f.setMinimumWidth(350)
w = QLabel(_('&File:'))
l.addRow(w, h), w.setBuddy(f)
self.paper_size = ps = QComboBox(self)
ps.addItems([a.upper() for a in sorted(self.paper_size_map, key=numeric_sort_key)])
previous_size = vprefs.get('print-to-pdf-page-size', None)
if previous_size not in self.paper_size_map:
previous_size = (QPrinter().pageLayout().pageSize().name() or '').lower()
if previous_size not in self.paper_size_map:
previous_size = 'a4'
ps.setCurrentIndex(ps.findText(previous_size.upper()))
l.addRow(_('Paper &size:'), ps)
tmap = {
'left':_('&Left margin:'),
'top':_('&Top margin:'),
'right':_('&Right margin:'),
'bottom':_('&Bottom margin:'),
}
for edge in 'left top right bottom'.split():
m = QDoubleSpinBox(self)
m.setSuffix(' ' + _('inches'))
m.setMinimum(0), m.setMaximum(3), m.setSingleStep(0.1)
val = vprefs.get('print-to-pdf-%s-margin' % edge, 1)
m.setValue(val)
setattr(self, '%s_margin' % edge, m)
l.addRow(tmap[edge], m)
self.pnum = pnum = QCheckBox(_('Add page &number to printed pages'), self)
pnum.setChecked(vprefs.get('print-to-pdf-page-numbers', True))
l.addRow(pnum)
self.show_file = sf = QCheckBox(_('&Open PDF file after printing'), self)
sf.setChecked(vprefs.get('print-to-pdf-show-file', True))
l.addRow(sf)
l.addRow(self.bb)
@property
def data(self):
fpath = self.file_name.text().strip()
head, tail = os.path.split(fpath)
tail = sanitize_file_name(tail)
fpath = tail
if head:
fpath = os.path.join(head, tail)
ans = {
'output': fpath,
'paper_size': self.paper_size.currentText().lower(),
'page_numbers':self.pnum.isChecked(),
'show_file':self.show_file.isChecked(),
}
for edge in 'left top right bottom'.split():
ans['margin_' + edge] = getattr(self, '%s_margin' % edge).value()
return ans
def choose_file(self):
ans = choose_save_file(self, self.OUTPUT_NAME, _('PDF file'), filters=[(_('PDF file'), ['pdf'])],
all_files=False, initial_filename=self.default_file_name)
if ans:
self.file_name.setText(ans)
def save_used_values(self):
data = self.data
vprefs['print-to-pdf-page-size'] = data['paper_size']
vprefs['print-to-pdf-page-numbers'] = data['page_numbers']
vprefs['print-to-pdf-show-file'] = data['show_file']
for edge in 'left top right bottom'.split():
vprefs['print-to-pdf-%s-margin' % edge] = data['margin_' + edge]
def accept(self):
fname = self.file_name.text().strip()
if not fname:
return error_dialog(self, _('No filename specified'), _(
'You must specify a filename for the PDF file to generate'), show=True)
if not fname.lower().endswith('.pdf'):
return error_dialog(self, _('Incorrect filename specified'), _(
'The filename for the PDF file must end with .pdf'), show=True)
self.save_used_values()
return Dialog.accept(self)
class DoPrint(Thread):
daemon = True
def __init__(self, data):
Thread.__init__(self, name='DoPrint')
self.data = data
self.tb = self.log = None
def run(self):
try:
with PersistentTemporaryFile('print-to-pdf-log.txt') as f:
p = self.worker = start_pipe_worker('from calibre.gui2.viewer.printing import do_print; do_print()', stdout=f, stderr=subprocess.STDOUT)
p.stdin.write(msgpack_dumps(self.data)), p.stdin.flush(), p.stdin.close()
rc = p.wait()
if rc != 0:
f.seek(0)
self.log = f.read().decode('utf-8', 'replace')
try:
os.remove(f.name)
except EnvironmentError:
pass
except Exception:
import traceback
self.tb = traceback.format_exc()
def do_print():
from calibre.customize.ui import plugin_for_input_format
stdin = getattr(sys.stdin, 'buffer', sys.stdin)
data = msgpack_loads(stdin.read())
ext = data['input'].lower().rpartition('.')[-1]
input_plugin = plugin_for_input_format(ext)
args = ['ebook-convert', data['input'], data['output'], '--paper-size', data['paper_size'], '--pdf-add-toc',
'--disable-remove-fake-margins', '--chapter-mark', 'none', '-vv']
if input_plugin.is_image_collection:
args.append('--no-process')
else:
args.append('--disable-font-rescaling')
args.append('--page-breaks-before=/')
if data['page_numbers']:
args.append('--pdf-page-numbers')
for edge in 'left top right bottom'.split():
args.append('--pdf-page-margin-' + edge), args.append('%.1f' % (data['margin_' + edge] * 72))
from calibre.ebooks.conversion.cli import main
main(args)
class Printing(QProgressDialog):
def __init__(self, thread, show_file, parent=None):
QProgressDialog.__init__(self, _('Printing, this will take a while, please wait...'), _('&Cancel'), 0, 0, parent)
self.show_file = show_file
self.setWindowTitle(_('Printing...'))
self.setWindowIcon(QIcon(I('print.png')))
self.thread = thread
self.timer = t = QTimer(self)
t.timeout.connect(self.check)
self.canceled.connect(self.do_cancel)
t.start(100)
def check(self):
if self.thread.is_alive():
return
if self.thread.tb or self.thread.log:
error_dialog(self, _('Failed to convert to PDF'), _(
'Failed to generate PDF file, click "Show details" for more information.'), det_msg=self.thread.tb or self.thread.log, show=True)
else:
if self.show_file:
open_local_file(self.thread.data['output'])
self.accept()
def do_cancel(self):
if hasattr(self.thread, 'worker'):
try:
if self.thread.worker.poll() is None:
self.thread.worker.kill()
except EnvironmentError:
import traceback
traceback.print_exc()
self.timer.stop()
self.reject()
def print_book(path_to_book, parent=None, book_title=None):
book_title = book_title or os.path.splitext(os.path.basename(path_to_book))[0]
d = PrintDialog(book_title, parent)
if d.exec_() == d.Accepted:
data = d.data
data['input'] = path_to_book
t = DoPrint(data)
t.start()
Printing(t, data['show_file'], parent).exec_()
if __name__ == '__main__':
app = Application([])
print_book(sys.argv[-1])
del app

View File

@ -1,82 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt5.Qt import (QDialog, QDialogButtonBox, QVBoxLayout, QApplication,
QSize, QIcon, Qt)
from PyQt5.QtWebKitWidgets import QWebView
from calibre.gui2 import gprefs, error_dialog
class TableView(QDialog):
def __init__(self, parent, font_magnification_step):
QDialog.__init__(self, parent)
self.font_magnification_step = font_magnification_step
dw = QApplication.instance().desktop()
self.avail_geom = dw.availableGeometry(parent)
self.view = QWebView(self)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Close)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
self.zi_button = zi = bb.addButton(_('Zoom &in'), bb.ActionRole)
self.zo_button = zo = bb.addButton(_('Zoom &out'), bb.ActionRole)
zi.setIcon(QIcon(I('plus.png')))
zo.setIcon(QIcon(I('minus.png')))
zi.clicked.connect(self.zoom_in)
zo.clicked.connect(self.zoom_out)
self.l = l = QVBoxLayout()
self.setLayout(l)
l.addWidget(self.view)
l.addWidget(bb)
def zoom_in(self):
self.view.setZoomFactor(self.view.zoomFactor() +
self.font_magnification_step)
def zoom_out(self):
self.view.setZoomFactor(max(0.1, self.view.zoomFactor() - self.font_magnification_step))
def __call__(self, html, baseurl):
self.view.setHtml(
'<!DOCTYPE html><html><body bgcolor="white">%s<body></html>'%html,
baseurl)
geom = self.avail_geom
self.resize(QSize(int(geom.width()/2.5), geom.height()-50))
geom = gprefs.get('viewer_table_popup_geometry', None)
if geom is not None:
self.restoreGeometry(geom)
self.setWindowTitle(_('View table'))
self.show()
def done(self, e):
gprefs['viewer_table_popup_geometry'] = bytearray(self.saveGeometry())
return QDialog.done(self, e)
class TablePopup(object):
def __init__(self, parent):
self.parent = parent
self.dialogs = []
def __call__(self, html, baseurl, font_magnification_step):
if not html:
return error_dialog(self.parent, _('No table found'),
_('No table was found'), show=True)
d = TableView(self.parent, font_magnification_step)
self.dialogs.append(d)
d.finished.connect(self.cleanup, type=Qt.QueuedConnection)
d(html, baseurl)
def cleanup(self):
for d in tuple(self.dialogs):
if not d.isVisible():
self.dialogs.remove(d)

View File

@ -1,411 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re
from functools import partial
from PyQt5.Qt import (
QStandardItem, QStandardItemModel, Qt, QFont, QTreeView, QWidget,
QHBoxLayout, QToolButton, QIcon, QModelIndex, pyqtSignal, QMenu,
QStyledItemDelegate, QToolTip, QApplication)
from calibre.ebooks.metadata.toc import TOC as MTOC
from calibre.gui2 import error_dialog
from calibre.gui2.search_box import SearchBox2
from calibre.utils.icu import primary_contains
from polyglot.builtins import iteritems
class Delegate(QStyledItemDelegate):
def helpEvent(self, ev, view, option, index):
# Show a tooltip only if the item is truncated
if not ev or not view:
return False
if ev.type() == ev.ToolTip:
rect = view.visualRect(index)
size = self.sizeHint(option, index)
if rect.width() < size.width():
tooltip = index.data(Qt.DisplayRole)
QToolTip.showText(ev.globalPos(), tooltip, view)
return True
return QStyledItemDelegate.helpEvent(self, ev, view, option, index)
class TOCView(QTreeView):
searched = pyqtSignal(object)
def __init__(self, *args):
QTreeView.__init__(self, *args)
self.delegate = Delegate(self)
self.setItemDelegate(self.delegate)
self.setMinimumWidth(80)
self.header().close()
self.setMouseTracking(True)
self.setStyleSheet('''
QTreeView {
background-color: palette(window);
color: palette(window-text);
border: none;
}
QTreeView::item {
border: 1px solid transparent;
padding-top:0.5ex;
padding-bottom:0.5ex;
}
QTreeView::item:hover {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
border: 1px solid #bfcde4;
border-radius: 6px;
}
''')
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.context_menu)
def mouseMoveEvent(self, ev):
if self.indexAt(ev.pos()).isValid():
self.setCursor(Qt.PointingHandCursor)
else:
self.unsetCursor()
return QTreeView.mouseMoveEvent(self, ev)
def expand_tree(self, index):
self.expand(index)
i = -1
while True:
i += 1
child = index.child(i, 0)
if not child.isValid():
break
self.expand_tree(child)
def context_menu(self, pos):
index = self.indexAt(pos)
m = QMenu(self)
if index.isValid():
m.addAction(_('Expand all items under %s') % index.data(), partial(self.expand_tree, index))
m.addSeparator()
m.addAction(_('Expand all items'), self.expandAll)
m.addAction(_('Collapse all items'), self.collapseAll)
m.addSeparator()
m.addAction(_('Copy table of contents to clipboard'), self.copy_to_clipboard)
m.exec_(self.mapToGlobal(pos))
def keyPressEvent(self, event):
try:
if self.handle_shortcuts(event):
return
except AttributeError:
pass
return QTreeView.keyPressEvent(self, event)
def copy_to_clipboard(self):
m = self.model()
QApplication.clipboard().setText(getattr(m, 'as_plain_text', ''))
class TOCSearch(QWidget):
def __init__(self, toc_view, parent=None):
QWidget.__init__(self, parent)
self.toc_view = toc_view
self.l = l = QHBoxLayout(self)
self.search = s = SearchBox2(self)
self.search.setMinimumContentsLength(15)
self.search.initialize('viewer_toc_search_history', help_text=_('Search Table of Contents'))
self.search.setToolTip(_('Search for text in the Table of Contents'))
s.search.connect(self.do_search)
self.go = b = QToolButton(self)
b.setIcon(QIcon(I('search.png')))
b.clicked.connect(s.do_search)
b.setToolTip(_('Find next match'))
l.addWidget(s), l.addWidget(b)
def do_search(self, text):
if not text or not text.strip():
return
index = self.toc_view.model().search(text)
if index.isValid():
self.toc_view.searched.emit(index)
else:
error_dialog(self.toc_view, _('No matches found'), _(
'There are no Table of Contents entries matching: %s') % text, show=True)
self.search.search_done(True)
class TOCItem(QStandardItem):
def __init__(self, spine, toc, depth, all_items, parent=None):
text = toc.text
if text:
text = re.sub(r'\s', ' ', text)
self.title = text
self.parent = parent
self.href = toc.href
QStandardItem.__init__(self, text if text else '')
self.abspath = toc.abspath if toc.href else None
self.fragment = toc.fragment
all_items.append(self)
self.emphasis_font = QFont(self.font())
self.emphasis_font.setBold(True), self.emphasis_font.setItalic(True)
self.normal_font = self.font()
for t in toc:
self.appendRow(TOCItem(spine, t, depth+1, all_items, parent=self))
self.setFlags(Qt.ItemIsEnabled)
self.is_current_search_result = False
spos = 0
for i, si in enumerate(spine):
if si == self.abspath:
spos = i
break
am = {}
if self.abspath is not None:
try:
am = getattr(spine[i], 'anchor_map', {})
except UnboundLocalError:
# Spine was empty?
pass
frag = self.fragment if (self.fragment and self.fragment in am) else None
self.starts_at = spos
self.start_anchor = frag
self.start_src_offset = am.get(frag, 0)
self.depth = depth
self.is_being_viewed = False
@property
def ancestors(self):
parent = self.parent
while parent is not None:
yield parent
parent = parent.parent
@classmethod
def type(cls):
return QStandardItem.UserType+10
def update_indexing_state(self, spine_index, viewport_rect, anchor_map,
in_paged_mode):
if in_paged_mode:
self.update_indexing_state_paged(spine_index, viewport_rect,
anchor_map)
else:
self.update_indexing_state_unpaged(spine_index, viewport_rect,
anchor_map)
def update_indexing_state_unpaged(self, spine_index, viewport_rect,
anchor_map):
is_being_viewed = False
top, bottom = viewport_rect[1], viewport_rect[3]
# We use bottom-25 in the checks below to account for the case where
# the next entry has some invisible margin that just overlaps with the
# bottom of the screen. In this case it will appear to the user that
# the entry is not visible on the screen. Of course, the margin could
# be larger than 25, but that's a decent compromise. Also we dont want
# to count a partial line as being visible.
# We only care about y position
anchor_map = {k:v[1] for k, v in iteritems(anchor_map)}
if spine_index >= self.starts_at and spine_index <= self.ends_at:
# The position at which this anchor is present in the document
start_pos = anchor_map.get(self.start_anchor, 0)
psp = []
if self.ends_at == spine_index:
# Anchors that could possibly indicate the start of the next
# section and therefore the end of this section.
# self.possible_end_anchors is a set of anchors belonging to
# toc entries with depth <= self.depth that are also not
# ancestors of this entry.
psp = [anchor_map.get(x, 0) for x in self.possible_end_anchors]
psp = [x for x in psp if x >= start_pos]
# The end position. The first anchor whose pos is >= start_pos
# or if the end is not in this spine item, we set it to the bottom
# of the window +1
end_pos = min(psp) if psp else (bottom+1 if self.ends_at >=
spine_index else 0)
if spine_index > self.starts_at and spine_index < self.ends_at:
# The entire spine item is contained in this entry
is_being_viewed = True
elif (spine_index == self.starts_at and bottom-25 >= start_pos and
# This spine item contains the start
# The start position is before the end of the viewport
(spine_index != self.ends_at or top < end_pos)):
# The end position is after the start of the viewport
is_being_viewed = True
elif (spine_index == self.ends_at and top < end_pos and
# This spine item contains the end
# The end position is after the start of the viewport
(spine_index != self.starts_at or bottom-25 >= start_pos)):
# The start position is before the end of the viewport
is_being_viewed = True
changed = is_being_viewed != self.is_being_viewed
self.is_being_viewed = is_being_viewed
if changed:
self.setFont(self.emphasis_font if is_being_viewed else self.normal_font)
def update_indexing_state_paged(self, spine_index, viewport_rect,
anchor_map):
is_being_viewed = False
left, right = viewport_rect[0], viewport_rect[2]
left, right = (left, 0), (right, -1)
if spine_index >= self.starts_at and spine_index <= self.ends_at:
# The position at which this anchor is present in the document
start_pos = anchor_map.get(self.start_anchor, (0, 0))
psp = []
if self.ends_at == spine_index:
# Anchors that could possibly indicate the start of the next
# section and therefore the end of this section.
# self.possible_end_anchors is a set of anchors belonging to
# toc entries with depth <= self.depth that are also not
# ancestors of this entry.
psp = [anchor_map.get(x, (0, 0)) for x in self.possible_end_anchors]
psp = [x for x in psp if x >= start_pos]
# The end position. The first anchor whose pos is >= start_pos
# or if the end is not in this spine item, we set it to the column
# after the right edge of the viewport
end_pos = min(psp) if psp else (right if self.ends_at >=
spine_index else (0, 0))
if spine_index > self.starts_at and spine_index < self.ends_at:
# The entire spine item is contained in this entry
is_being_viewed = True
elif (spine_index == self.starts_at and right > start_pos and
# This spine item contains the start
# The start position is before the end of the viewport
(spine_index != self.ends_at or left < end_pos)):
# The end position is after the start of the viewport
is_being_viewed = True
elif (spine_index == self.ends_at and left < end_pos and
# This spine item contains the end
# The end position is after the start of the viewport
(spine_index != self.starts_at or right > start_pos)):
# The start position is before the end of the viewport
is_being_viewed = True
changed = is_being_viewed != self.is_being_viewed
self.is_being_viewed = is_being_viewed
if changed:
self.setFont(self.emphasis_font if is_being_viewed else self.normal_font)
def set_current_search_result(self, yes):
if yes and not self.is_current_search_result:
self.setText(self.text() + '')
self.is_current_search_result = True
elif not yes and self.is_current_search_result:
self.setText(self.text()[:-2])
self.is_current_search_result = False
def __repr__(self):
return 'TOC Item: %s %s#%s'%(self.title, self.abspath, self.fragment)
def __str__(self):
return repr(self)
class TOC(QStandardItemModel):
def __init__(self, spine, toc=None):
QStandardItemModel.__init__(self)
self.current_query = {'text':'', 'index':-1, 'items':()}
if toc is None:
toc = MTOC()
self.all_items = depth_first = []
for t in toc:
self.appendRow(TOCItem(spine, t, 0, depth_first))
for x in depth_first:
possible_enders = [t for t in depth_first if t.depth <= x.depth and
t.starts_at >= x.starts_at and t is not x and t not in
x.ancestors]
if possible_enders:
min_spine = min(t.starts_at for t in possible_enders)
possible_enders = {t.fragment for t in possible_enders if
t.starts_at == min_spine}
else:
min_spine = len(spine) - 1
possible_enders = set()
x.ends_at = min_spine
x.possible_end_anchors = possible_enders
self.currently_viewed_entry = None
def update_indexing_state(self, *args):
items_being_viewed = []
for t in self.all_items:
t.update_indexing_state(*args)
if t.is_being_viewed:
items_being_viewed.append(t)
self.currently_viewed_entry = t
return items_being_viewed
def next_entry(self, spine_pos, anchor_map, viewport_rect, in_paged_mode,
backwards=False, current_entry=None):
current_entry = (self.currently_viewed_entry if current_entry is None
else current_entry)
if current_entry is None:
return
items = reversed(self.all_items) if backwards else self.all_items
found = False
if in_paged_mode:
start = viewport_rect[0]
anchor_map = {k:v[0] for k, v in iteritems(anchor_map)}
else:
start = viewport_rect[1]
anchor_map = {k:v[1] for k, v in iteritems(anchor_map)}
for item in items:
if found:
start_pos = anchor_map.get(item.start_anchor, 0)
if backwards and item.is_being_viewed and start_pos >= start:
# This item will not cause any scrolling
continue
if item.starts_at != spine_pos or item.start_anchor:
return item
if item is current_entry:
found = True
def find_items(self, query):
for item in self.all_items:
if primary_contains(query, item.text()):
yield item
def find_indices_by_href(self, query):
for item in self.all_items:
q = (item.href or '')
if item.fragment:
q += '#' + item.fragment
if primary_contains(query, q):
yield self.indexFromItem(item)
def search(self, query):
cq = self.current_query
if cq['items'] and -1 < cq['index'] < len(cq['items']):
cq['items'][cq['index']].set_current_search_result(False)
if cq['text'] != query:
items = tuple(self.find_items(query))
cq.update({'text':query, 'items':items, 'index':-1})
if len(cq['items']) > 0:
cq['index'] = (cq['index'] + 1) % len(cq['items'])
item = cq['items'][cq['index']]
item.set_current_search_result(True)
index = self.indexFromItem(item)
return index
return QModelIndex()
@property
def as_plain_text(self):
lines = []
for item in self.all_items:
lines.append(' ' * (4 * item.depth) + (item.title or ''))
return '\n'.join(lines)

View File

@ -1,425 +0,0 @@
#!/usr/bin/env python2
# vim:fileencoding=utf-8
from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import textwrap
from PyQt5.Qt import (
QIcon, QWidget, Qt, QGridLayout, QScrollBar, QToolBar, QAction,
QToolButton, QMenu, QDoubleSpinBox, pyqtSignal, QLineEdit,
QRegExpValidator, QRegExp, QPalette, QColor, QBrush, QPainter,
QDockWidget, QSize, QWebView, QLabel, QVBoxLayout)
from calibre.gui2 import rating_font, error_dialog, safe_open_url
from calibre.gui2.main_window import MainWindow
from calibre.gui2.search_box import SearchBox2
from calibre.gui2.viewer.documentview import DocumentView
from calibre.gui2.viewer.bookmarkmanager import BookmarkManager
from calibre.gui2.viewer.toc import TOCView, TOCSearch
from calibre.gui2.viewer.footnote import FootnotesView
from calibre.utils.localization import is_rtl
from polyglot.builtins import unicode_type, range
class DoubleSpinBox(QDoubleSpinBox): # {{{
value_changed = pyqtSignal(object, object)
def __init__(self, *args, **kwargs):
QDoubleSpinBox.__init__(self, *args, **kwargs)
self.tt = _('Position in book')
self.setToolTip(self.tt)
def set_value(self, val):
self.blockSignals(True)
self.setValue(val)
try:
self.setToolTip(self.tt +
' [{0:.0%}]'.format(float(val)/self.maximum()))
except ZeroDivisionError:
self.setToolTip(self.tt)
self.blockSignals(False)
self.value_changed.emit(self.value(), self.maximum())
# }}}
class Reference(QLineEdit): # {{{
goto = pyqtSignal(object)
def __init__(self, *args):
QLineEdit.__init__(self, *args)
self.setValidator(QRegExpValidator(QRegExp(r'\d+\.\d+'), self))
self.setToolTip(textwrap.fill('<p>'+_(
'Go to a reference. To get reference numbers, use the <i>reference '
'mode</i>, by clicking the reference mode button in the toolbar.')))
if hasattr(self, 'setPlaceholderText'):
self.setPlaceholderText(_('Go to a reference number...'))
self.editingFinished.connect(self.editing_finished)
def editing_finished(self):
text = unicode_type(self.text())
self.setText('')
self.goto.emit(text)
# }}}
class Metadata(QWebView): # {{{
def __init__(self, parent):
QWebView.__init__(self, parent)
s = self.settings()
s.setAttribute(s.JavascriptEnabled, False)
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
self.page().linkClicked.connect(self.link_clicked)
self.setAttribute(Qt.WA_OpaquePaintEvent, False)
palette = self.palette()
palette.setBrush(QPalette.Base, Qt.transparent)
self.page().setPalette(palette)
self.setVisible(False)
def link_clicked(self, qurl):
if qurl.scheme() in ('http', 'https'):
return safe_open_url(qurl)
def update_layout(self):
self.setGeometry(0, 0, self.parent().width(), self.parent().height())
def show_metadata(self, mi, ext=''):
from calibre.gui2 import default_author_link
from calibre.gui2.book_details import render_html, css
from calibre.ebooks.metadata.book.render import mi_to_html
def render_data(mi, use_roman_numbers=True, all_fields=False, pref_name='book_display_fields'):
return mi_to_html(
mi, use_roman_numbers=use_roman_numbers, rating_font=rating_font(), rtl=is_rtl(),
default_author_link=default_author_link()
)
html = render_html(mi, css(), True, self, render_data_func=render_data)
self.setHtml(html)
def setVisible(self, x):
if x:
self.update_layout()
QWebView.setVisible(self, x)
def paintEvent(self, ev):
p = QPainter(self)
p.fillRect(ev.region().boundingRect(), QBrush(QColor(200, 200, 200, 247), Qt.SolidPattern))
p.end()
QWebView.paintEvent(self, ev)
# }}}
class History(list): # {{{
def __init__(self, action_back=None, action_forward=None):
self.action_back = action_back
self.action_forward = action_forward
super(History, self).__init__(self)
self.clear()
def clear(self):
del self[:]
self.insert_pos = 0
self.back_pos = None
self.forward_pos = None
self.set_actions()
def set_actions(self):
if self.action_back is not None:
self.action_back.setDisabled(self.back_pos is None)
if self.action_forward is not None:
self.action_forward.setDisabled(self.forward_pos is None)
def back(self, item_when_clicked):
# Back clicked
if self.back_pos is None:
return None
item = self[self.back_pos]
self.forward_pos = self.back_pos+1
if self.forward_pos >= len(self):
# We are at the head of the stack, append item to the stack so that
# clicking forward again will take us to where we were when we
# clicked back
self.append(item_when_clicked)
self.forward_pos = len(self) - 1
self.insert_pos = self.forward_pos
self.back_pos = None if self.back_pos == 0 else self.back_pos - 1
self.set_actions()
return item
def forward(self, item_when_clicked):
# Forward clicked
if self.forward_pos is None:
return None
item = self[self.forward_pos]
self.back_pos = self.forward_pos - 1
if self.back_pos < 0:
self.back_pos = None
self.insert_pos = min(len(self) - 1, (self.back_pos or 0) + 1)
self.forward_pos = None if self.forward_pos > len(self) - 2 else self.forward_pos + 1
self.set_actions()
return item
def add(self, item):
# Link clicked
self[self.insert_pos:] = []
while self.insert_pos > 0 and self[self.insert_pos-1] == item:
self.insert_pos -= 1
self[self.insert_pos:] = []
self.insert(self.insert_pos, item)
# The next back must go to item
self.back_pos = self.insert_pos
self.insert_pos += 1
# There can be no forward
self.forward_pos = None
self.set_actions()
def __str__(self):
return 'History: Items=%s back_pos=%s insert_pos=%s forward_pos=%s' % (tuple(self), self.back_pos, self.insert_pos, self.forward_pos)
def test_history():
h = History()
for i in range(4):
h.add(i)
for i in reversed(h):
h.back(i)
h.forward(0)
h.add(9)
assert h == [0, 9]
# }}}
class ToolBar(QToolBar): # {{{
def contextMenuEvent(self, ev):
ac = self.actionAt(ev.pos())
if ac is None:
return
ch = self.widgetForAction(ac)
sm = getattr(ch, 'showMenu', None)
if callable(sm):
ev.accept()
sm()
# }}}
class Main(MainWindow):
def __init__(self, debug_javascript):
MainWindow.__init__(self, None)
self.setWindowTitle(_('E-book viewer'))
self.base_window_title = unicode_type(self.windowTitle())
self.setObjectName('EbookViewer')
self.setWindowIcon(QIcon(I('viewer.png')))
self.setDockOptions(self.AnimatedDocks | self.AllowTabbedDocks)
self.centralwidget = c = QWidget(self)
c.setObjectName('centralwidget')
self.setCentralWidget(c)
self.central_layout = cl = QGridLayout(c)
cl.setSpacing(0)
c.setLayout(cl), cl.setContentsMargins(0, 0, 0, 0)
self.view = v = DocumentView(self)
v.setMinimumSize(100, 100)
self.view.initialize_view(debug_javascript)
v.setObjectName('view')
cl.addWidget(v)
self.vertical_scrollbar = vs = QScrollBar(c)
vs.setOrientation(Qt.Vertical), vs.setObjectName("vertical_scrollbar")
cl.addWidget(vs, 0, 1, 2, 1)
self.horizontal_scrollbar = hs = QScrollBar(c)
hs.setOrientation(Qt.Horizontal), hs.setObjectName("horizontal_scrollbar")
cl.addWidget(hs, 1, 0, 1, 1)
self.tool_bar = tb = ToolBar(self)
tb.setObjectName('tool_bar'), tb.setIconSize(QSize(32, 32))
self.addToolBar(Qt.LeftToolBarArea, tb)
self.tool_bar2 = tb2 = QToolBar(self)
tb2.setObjectName('tool_bar2')
self.addToolBar(Qt.TopToolBarArea, tb2)
self.tool_bar.setContextMenuPolicy(Qt.DefaultContextMenu)
self.tool_bar2.setContextMenuPolicy(Qt.PreventContextMenu)
self.pos = DoubleSpinBox()
self.pos.setDecimals(1)
self.pos.setSuffix('/'+_('Unknown')+' ')
self.pos.setMinimum(1.)
self.tool_bar2.addWidget(self.pos)
self.tool_bar2.addSeparator()
self.reference = Reference()
self.tool_bar2.addWidget(self.reference)
self.tool_bar2.addSeparator()
self.search = SearchBox2(self)
self.search.setMinimumContentsLength(20)
self.search.initialize('viewer_search_history')
self.search.setToolTip(_('Search for text in book'))
self.search.setMinimumWidth(200)
self.tool_bar2.addWidget(self.search)
self.toc_dock = d = QDockWidget(_('Table of Contents'), self)
d.setContextMenuPolicy(Qt.CustomContextMenu)
self.toc_container = w = QWidget(self)
w.l = QVBoxLayout(w)
self.toc = TOCView(w)
self.toc_search = TOCSearch(self.toc, parent=w)
w.l.addWidget(self.toc), w.l.addWidget(self.toc_search), w.l.setContentsMargins(0, 0, 0, 0)
d.setObjectName('toc-dock')
d.setWidget(w)
d.close() # starts out hidden
self.addDockWidget(Qt.LeftDockWidgetArea, d)
d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.bookmarks_dock = d = QDockWidget(_('Bookmarks'), self)
d.setContextMenuPolicy(Qt.CustomContextMenu)
self.bookmarks = BookmarkManager(self)
d.setObjectName('bookmarks-dock')
d.setWidget(self.bookmarks)
d.close() # starts out hidden
self.addDockWidget(Qt.RightDockWidgetArea, d)
d.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.footnotes_dock = d = QDockWidget(_('Footnotes'), self)
d.setContextMenuPolicy(Qt.CustomContextMenu)
self.footnotes_view = FootnotesView(self)
self.footnotes_view.follow_link.connect(self.view.follow_footnote_link)
self.footnotes_view.close_view.connect(d.close)
self.view.footnotes.set_footnotes_view(self.footnotes_view)
d.setObjectName('footnotes-dock')
d.setWidget(self.footnotes_view)
d.close() # starts out hidden
self.addDockWidget(Qt.BottomDockWidgetArea, d)
d.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea | Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.create_actions()
self.themes_menu.aboutToShow.connect(self.themes_menu_shown, type=Qt.QueuedConnection)
self.metadata = Metadata(self.centralwidget)
self.history = History(self.action_back, self.action_forward)
self.full_screen_label = QLabel('''
<center>
<h1>%s</h1>
<h3>%s</h3>
<h3>%s</h3>
<h3>%s</h3>
</center>
'''%(_('Full screen mode'),
_('Right click to show controls'),
_('Tap in the left or right page margin to turn pages'),
_('Press Esc to quit')),
self.centralWidget())
self.full_screen_label.setVisible(False)
self.full_screen_label.final_height = 200
self.full_screen_label.setFocusPolicy(Qt.NoFocus)
self.full_screen_label.setStyleSheet('''
QLabel {
text-align: center;
background-color: white;
color: black;
border-width: 1px;
border-style: solid;
border-radius: 20px;
}
''')
self.clock_label = QLabel('99:99', self.centralWidget())
self.clock_label.setVisible(False)
self.clock_label.setFocusPolicy(Qt.NoFocus)
self.info_label_style = '''
QLabel {
text-align: center;
border-width: 1px;
border-style: solid;
border-radius: 8px;
background-color: %s;
color: %s;
font-family: monospace;
font-size: larger;
padding: 5px;
}'''
self.pos_label = QLabel('2000/4000', self.centralWidget())
self.pos_label.setVisible(False)
self.pos_label.setFocusPolicy(Qt.NoFocus)
self.resize(653, 746)
def resizeEvent(self, ev):
if self.metadata.isVisible():
self.metadata.update_layout()
return MainWindow.resizeEvent(self, ev)
def initialize_dock_state(self):
self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea)
self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea)
self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)
self.footnotes_dock.close()
def themes_menu_shown(self):
if len(self.themes_menu.actions()) == 0:
self.themes_menu.hide()
error_dialog(self, _('No themes'), _(
'You must first create some themes in the viewer preferences'), show=True)
def create_actions(self):
def a(name, text, icon, tb=None, sc_name=None, menu_name=None, popup_mode=QToolButton.MenuButtonPopup):
name = 'action_' + name
if isinstance(text, QDockWidget):
ac = text.toggleViewAction()
ac.setIcon(QIcon(I(icon)))
else:
ac = QAction(QIcon(I(icon)), text, self)
setattr(self, name, ac)
ac.setObjectName(name)
(tb or self.tool_bar).addAction(ac)
if sc_name:
ac.setToolTip(unicode_type(ac.text()) + (' [%s]' % _(' or ').join(self.view.shortcuts.get_shortcuts(sc_name))))
if menu_name is not None:
menu_name += '_menu'
m = QMenu()
setattr(self, menu_name, m)
ac.setMenu(m)
w = (tb or self.tool_bar).widgetForAction(ac)
w.setPopupMode(popup_mode)
return ac
a('back', _('Back'), 'back.png')
a('forward', _('Forward'), 'forward.png')
self.tool_bar.addSeparator()
a('open_ebook', _('Open e-book'), 'document_open.png', menu_name='open_history')
a('copy', _('Copy to clipboard'), 'edit-copy.png').setDisabled(True)
a('font_size_larger', _('Increase font size'), 'font_size_larger.png')
a('font_size_smaller', _('Decrease font size'), 'font_size_smaller.png')
a('table_of_contents', self.toc_dock, 'toc.png', sc_name='Table of Contents')
a('full_screen', _('Toggle full screen'), 'page.png', sc_name='Fullscreen').setCheckable(True)
self.tool_bar.addSeparator()
a('previous_page', _('Previous page'), 'previous.png')
a('next_page', _('Next page'), 'next.png')
self.tool_bar.addSeparator()
a('bookmark', _('Bookmark'), 'bookmarks.png', menu_name='bookmarks', popup_mode=QToolButton.InstantPopup)
a('reference_mode', _('Reference mode'), 'lookfeel.png').setCheckable(True)
self.tool_bar.addSeparator()
a('preferences', _('Preferences'), 'config.png')
a('metadata', _('Show book metadata'), 'metadata.png').setCheckable(True)
a('load_theme', _('Load a theme'), 'wizard.png', menu_name='themes', popup_mode=QToolButton.InstantPopup)
self.tool_bar.addSeparator()
a('print', _('Print to PDF file'), 'print.png')
a('find_next', _('Find next occurrence'), 'arrow-down.png', tb=self.tool_bar2)
a('find_previous', _('Find previous occurrence'), 'arrow-up.png', tb=self.tool_bar2)
a('toggle_paged_mode', _('Toggle paged mode'), 'scroll.png', tb=self.tool_bar2).setCheckable(True)

View File

@ -10,7 +10,7 @@ __docformat__ = 'restructuredtext en'
import sys, os
from calibre import config_dir
from polyglot.builtins import builtins, itervalues
from polyglot.builtins import builtins
class PathResolver(object):
@ -83,72 +83,5 @@ def get_image_path(path, data=False, allow_user_override=True):
return get_path('images/'+path, data=data, allow_user_override=allow_user_override)
def js_name_to_path(name, ext='.coffee'):
path = ('/'.join(name.split('.'))) + ext
d = os.path.dirname
base = d(d(os.path.abspath(__file__)))
return os.path.join(base, path)
def _compile_coffeescript(name):
from calibre.utils.serve_coffee import compile_coffeescript
src = js_name_to_path(name)
with open(src, 'rb') as f:
cs, errors = compile_coffeescript(f.read(), src)
if errors:
for line in errors:
print(line)
raise Exception('Failed to compile coffeescript'
': %s'%src)
return cs
def compiled_coffeescript(name, dynamic=False):
import zipfile
zipf = get_path('compiled_coffeescript.zip', allow_user_override=False)
with zipfile.ZipFile(zipf, 'r') as zf:
if dynamic:
import json
existing_hash = json.loads(zf.comment or '{}').get(name + '.js')
if existing_hash is not None:
import hashlib
with open(js_name_to_path(name), 'rb') as f:
if existing_hash == hashlib.sha1(f.read()).hexdigest():
return zf.read(name + '.js')
return _compile_coffeescript(name)
else:
return zf.read(name+'.js')
def load_hyphenator_dicts(hp_cache, lang, default_lang='en'):
from calibre.utils.localization import lang_as_iso639_1
import zipfile
if not lang:
lang = default_lang or 'en'
def lang_name(l):
l = l.lower()
l = lang_as_iso639_1(l)
if not l:
l = 'en'
l = {'en':'en-us', 'nb':'nb-no', 'el':'el-monoton'}.get(l, l)
return l.lower().replace('_', '-')
if not hp_cache:
with zipfile.ZipFile(P('viewer/hyphenate/patterns.zip',
allow_user_override=False), 'r') as zf:
for pat in zf.namelist():
raw = zf.read(pat).decode('utf-8')
hp_cache[pat.partition('.')[0]] = raw
if lang_name(lang) not in hp_cache:
lang = lang_name(default_lang)
lang = lang_name(lang)
js = '\n\n'.join(itervalues(hp_cache))
return js, lang
builtins.__dict__['P'] = get_path
builtins.__dict__['I'] = get_image_path

File diff suppressed because one or more lines are too long