2
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
3
import sys, logging, os, traceback, time
5
from PyQt4.QtGui import QKeySequence, QPainter, QDialog, QSpinBox, QSlider, QIcon
6
from PyQt4.QtCore import Qt, QObject, SIGNAL, QCoreApplication, QThread
8
from calibre import __appname__, setup_cli_handlers, islinux
9
from calibre.ebooks.lrf.lrfparser import LRFDocument
11
from calibre.gui2 import ORG_NAME, APP_UID, error_dialog, \
12
config, choose_files, Application
13
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
14
from calibre.gui2.lrf_renderer.main_ui import Ui_MainWindow
15
from calibre.gui2.lrf_renderer.config_ui import Ui_ViewerConfig
16
from calibre.gui2.main_window import MainWindow
17
from calibre.gui2.lrf_renderer.document import Document
18
from calibre.gui2.library import SearchBox
20
class RenderWorker(QThread):
22
def __init__(self, parent, lrf_stream, logger, opts):
23
QThread.__init__(self, parent)
24
self.stream, self.logger, self.opts = lrf_stream, logger, opts
32
self.lrf = LRFDocument(self.stream)
38
except Exception, err:
39
self.lrf, self.stream = None, None
41
self.formatted_traceback = traceback.format_exc()
44
if self.lrf is not None:
46
self.lrf.keep_parsing = False
48
class Config(QDialog, Ui_ViewerConfig):
50
def __init__(self, parent, opts):
51
QDialog.__init__(self, parent)
52
Ui_ViewerConfig.__init__(self)
54
self.white_background.setChecked(opts.white_background)
55
self.hyphenate.setChecked(opts.hyphenate)
57
class Main(MainWindow, Ui_MainWindow):
59
def __init__(self, logger, opts, parent=None):
60
MainWindow.__init__(self, opts, parent)
61
Ui_MainWindow.__init__(self)
63
self.setAttribute(Qt.WA_DeleteOnClose)
64
self.setWindowTitle(__appname__ + _(' - LRF Viewer'))
69
self.document = Document(self.logger, self.opts)
70
self.spin_box_action = self.spin_box = QSpinBox()
71
self.tool_bar.addWidget(self.spin_box)
72
self.tool_bar.addSeparator()
73
self.slider_action = self.slider = QSlider(Qt.Horizontal)
74
self.tool_bar.addWidget(self.slider)
75
self.tool_bar.addSeparator()
76
self.search = SearchBox(self)
77
self.search_action = self.tool_bar.addWidget(self.search)
78
QObject.connect(self.document, SIGNAL('chapter_rendered(int)'), self.chapter_rendered)
79
QObject.connect(self.document, SIGNAL('page_changed(PyQt_PyObject)'), self.page_changed)
81
QObject.connect(self.search, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self.find)
83
self.action_next_page.setShortcuts(QKeySequence.MoveToNextPage)
84
self.action_previous_page.setShortcuts(QKeySequence.MoveToPreviousPage)
85
self.action_next_match.setShortcuts(QKeySequence.FindNext)
86
self.addAction(self.action_next_match)
87
QObject.connect(self.action_next_page, SIGNAL('triggered(bool)'), self.next)
88
QObject.connect(self.action_previous_page, SIGNAL('triggered(bool)'), self.previous)
89
QObject.connect(self.action_back, SIGNAL('triggered(bool)'), self.back)
90
QObject.connect(self.action_forward, SIGNAL('triggered(bool)'), self.forward)
91
QObject.connect(self.action_next_match, SIGNAL('triggered(bool)'), self.next_match)
92
QObject.connect(self.action_open_ebook, SIGNAL('triggered(bool)'), self.open_ebook)
93
QObject.connect(self.action_configure, SIGNAL('triggered(bool)'), self.configure)
94
QObject.connect(self.spin_box, SIGNAL('valueChanged(int)'), self.go_to_page)
95
QObject.connect(self.slider, SIGNAL('valueChanged(int)'), self.go_to_page)
98
self.graphics_view.setRenderHint(QPainter.Antialiasing, True)
99
self.graphics_view.setRenderHint(QPainter.TextAntialiasing, True)
100
self.graphics_view.setRenderHint(QPainter.SmoothPixmapTransform, True)
105
def configure(self, triggered):
106
opts = config['LRF_ebook_viewer_options']
109
d = Config(self, opts)
111
if d.result() == QDialog.Accepted:
112
opts.white_background = bool(d.white_background.isChecked())
113
opts.hyphenate = bool(d.hyphenate.isChecked())
114
config['LRF_ebook_viewer_options'] = opts
116
def set_ebook(self, stream):
117
self.progress_bar.setMinimum(0)
118
self.progress_bar.setMaximum(0)
119
self.progress_bar.setValue(0)
121
if stream is not None:
122
self.file_name = os.path.basename(stream.name) if hasattr(stream, 'name') else ''
123
self.progress_label.setText('Parsing '+ self.file_name)
124
self.renderer = RenderWorker(self, stream, self.logger, self.opts)
125
QObject.connect(self.renderer, SIGNAL('finished()'), self.parsed, Qt.QueuedConnection)
126
self.search.help_text = 'Search'
127
self.search.clear_to_help()
128
self.last_search = None
130
self.stack.setCurrentIndex(0)
133
def open_ebook(self, triggered):
134
files = choose_files(self, 'open ebook dialog', 'Choose ebook',
135
[('Ebooks', ['lrf'])], all_files=False,
136
select_only_single_file=True)
139
self.set_ebook(open(file, 'rb'))
143
def page_changed(self, num):
144
self.slider.setValue(num)
145
self.spin_box.setValue(num)
148
if self.renderer is not None:
149
self.stack.setCurrentIndex(1)
150
self.renderer.start()
152
def find(self, search, refinement):
153
self.last_search = search
155
self.document.search(search)
156
except StopIteration:
157
error_dialog(self, _('No matches found'), _('<b>No matches</b> for the search phrase <i>%s</i> were found.')%(search,)).exec_()
160
if not self.renderer.aborted and self.renderer.lrf is not None:
161
width, height = self.renderer.lrf.device_info.width, \
162
self.renderer.lrf.device_info.height
163
hdelta = self.tool_bar.height()+3
165
from PyQt4.QtGui import QScrollBar
167
scrollbar_adjust = min(s.width(), s.height())
168
self.graphics_view.resize_for(width+scrollbar_adjust, height+scrollbar_adjust)
170
desktop = QCoreApplication.instance().desktop()
171
screen_height = desktop.availableGeometry(self).height() - 25
172
height = min(screen_height, height+hdelta+scrollbar_adjust)
173
self.resize(width+scrollbar_adjust, height)
174
self.setWindowTitle(self.renderer.lrf.metadata.title + ' - ' + __appname__)
175
self.document_title = self.renderer.lrf.metadata.title
176
if self.opts.profile:
178
lrf = self.renderer.lrf
179
cProfile.runctx('self.document.render(lrf)', globals(), locals(), lrf.metadata.title+'.stats')
180
print 'Stats written to', self.renderer.lrf.metadata.title+'.stats'
183
self.document.render(self.renderer.lrf)
184
print 'Layout time:', time.time()-start, 'seconds'
185
self.renderer.lrf = None
187
self.graphics_view.setScene(self.document)
188
self.graphics_view.show()
189
self.spin_box.setRange(1, self.document.num_of_pages)
190
self.slider.setRange(1, self.document.num_of_pages)
191
self.spin_box.setSuffix(' of %d'%(self.document.num_of_pages,))
192
self.spin_box.updateGeometry()
193
self.stack.setCurrentIndex(0)
194
elif self.renderer.exception is not None:
195
exception = self.renderer.exception
196
print >>sys.stderr, 'Error rendering document'
197
print >>sys.stderr, exception
198
print >>sys.stderr, self.renderer.formatted_traceback
199
msg = u'<p><b>%s</b>: '%(exception.__class__.__name__,) + unicode(str(exception), 'utf8', 'replace') + u'</p>'
200
msg += u'<p>Failed to render document</p>'
201
msg += u'<p>Detailed <b>traceback</b>:<pre>'
202
msg += self.renderer.formatted_traceback + '</pre>'
203
d = ConversionErrorDialog(self, 'Error while rendering file', msg)
206
def chapter_rendered(self, num):
208
self.progress_bar.setMinimum(0)
209
self.progress_bar.setMaximum(num)
210
self.progress_bar.setValue(0)
211
self.progress_label.setText('Laying out '+ self.document_title)
213
self.progress_bar.setValue(self.progress_bar.value()+1)
214
QCoreApplication.processEvents()
216
def next(self, triggered):
219
def next_match(self, triggered):
221
self.document.next_match()
222
except StopIteration:
225
def previous(self, triggered):
226
self.document.previous()
228
def go_to_page(self, num):
229
self.document.show_page(num)
231
def forward(self, triggered):
232
self.document.forward()
234
def back(self, triggered):
237
def wheelEvent(self, ev):
239
self.document.previous()
243
def closeEvent(self, event):
244
if self.renderer is not None and self.renderer.isRunning():
245
self.renderer.abort()
247
self.emit(SIGNAL('viewer_closed(PyQt_PyObject)'), self)
251
def file_renderer(stream, opts, parent=None, logger=None):
253
level = logging.DEBUG if opts.verbose else logging.INFO
254
logger = logging.getLogger('lrfviewer')
255
setup_cli_handlers(logger, level)
257
try: # Set lrfviewer as the default for LRF files for this user
258
from subprocess import call
259
call('xdg-mime default calibre-lrfviewer.desktop application/lrf', shell=True)
262
m = Main(logger, opts, parent=parent)
268
from calibre.gui2.main_window import option_parser
269
parser = option_parser('''\
270
%prog [options] book.lrf
272
Read the LRF ebook book.lrf
274
parser.add_option('--verbose', default=False, action='store_true', dest='verbose',
275
help='Print more information about the rendering process')
276
parser.add_option('--visual-debug', help='Turn on visual aids to debugging the rendering engine',
277
default=False, action='store_true', dest='visual_debug')
278
parser.add_option('--disable-hyphenation', dest='hyphenate', default=True, action='store_false',
279
help='Disable hyphenation. Should significantly speed up rendering.')
280
parser.add_option('--white-background', dest='white_background', default=False, action='store_true',
281
help='By default the background is off white as I find this easier on the eyes. Use this option to make the background pure white.')
282
parser.add_option('--profile', dest='profile', default=False, action='store_true',
283
help='Profile the LRF renderer')
286
def normalize_settings(parser, opts):
287
saved_opts = config['LRF_ebook_viewer_options']
290
for opt in parser.option_list:
293
if getattr(opts, opt.dest) == opt.default and hasattr(saved_opts, opt.dest):
295
setattr(saved_opts, opt.dest, getattr(opts, opt.dest))
299
def main(args=sys.argv, logger=None):
300
parser = option_parser()
301
opts, args = parser.parse_args(args)
302
if hasattr(opts, 'help'):
305
pid = os.fork() if islinux else -1
307
app = Application(args)
308
app.setWindowIcon(QIcon(':/images/viewer.svg'))
309
QCoreApplication.setOrganizationName(ORG_NAME)
310
QCoreApplication.setApplicationName(APP_UID)
311
opts = normalize_settings(parser, opts)
312
stream = open(args[1], 'rb') if len(args) > 1 else None
313
main = file_renderer(stream, opts, logger=logger)
314
sys.excepthook = main.unhandled_exception
320
if __name__ == '__main__':