## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''''' import collections, itertools from PyQt4.QtCore import Qt, QByteArray, SIGNAL from PyQt4.QtGui import QGraphicsRectItem, QGraphicsScene, QPen, \ QBrush, QColor, QFontDatabase, \ QGraphicsItem, QGraphicsLineItem from libprs500.gui2.lrf_renderer.text import TextBlock, FontLoader, COLOR, PixmapItem from libprs500.ebooks.lrf.objects import RuledLine as _RuledLine from libprs500.ebooks.lrf.objects import Canvas as __Canvas class Color(QColor): def __init__(self, color): QColor.__init__(self, color.r, color.g, color.b, 0xff-color.a) class Pen(QPen): def __init__(self, color, width): QPen.__init__(self, QBrush(Color(color)), width, (Qt.SolidLine if width > 0 else Qt.NoPen)) class ContentObject(object): has_content = True def reset(self): self.has_content = True class RuledLine(QGraphicsLineItem, ContentObject): map = {'solid': Qt.SolidLine, 'dashed': Qt.DashLine, 'dotted': Qt.DotLine, 'double': Qt.DashDotLine} def __init__(self, rl): QGraphicsLineItem.__init__(self, 0, 0, rl.linelength, 0) ContentObject.__init__(self) self.setPen(QPen(COLOR(rl.linecolor, None), rl.linewidth, )) class ImageBlock(PixmapItem, ContentObject): def __init__(self, obj): ContentObject.__init__(self) x0, y0, x1, y1 = obj.attrs['x0'], obj.attrs['y0'], obj.attrs['x1'], obj.attrs['y1'] xsize, ysize, refstream = obj.attrs['xsize'], obj.attrs['ysize'], obj.refstream data, encoding = refstream.stream, refstream.encoding PixmapItem.__init__(self, data, encoding, x0, y0, x1, y1, xsize, ysize) self.block_id = obj.id def object_factory(container, obj, respect_max_y=False): if hasattr(obj, 'name'): if obj.name.endswith('TextBlock'): return TextBlock(obj, container.font_loader, respect_max_y, container.text_width, container.logger, container.opts, container.ruby_tags, container.link_activated) elif obj.name.endswith('ImageBlock'): return ImageBlock(obj) elif isinstance(obj, _RuledLine): return RuledLine(obj) elif isinstance(obj, __Canvas): return Canvas(container.font_loader, obj, container.logger, container.opts, container.ruby_tags, container.link_activated) return None class _Canvas(QGraphicsRectItem): def __init__(self, font_loader, logger, opts, width=0, height=0, parent=None, x=0, y=0): QGraphicsRectItem.__init__(self, x, y, width, height, parent) self.font_loader, self.logger, self.opts = font_loader, logger, opts self.current_y, self.max_y, self.max_x = 0, height, width self.is_full = False pen = QPen() pen.setStyle(Qt.NoPen) self.setPen(pen) def layout_block(self, block, x, y): if isinstance(block, TextBlock): self.layout_text_block(block, x, y) elif isinstance(block, RuledLine): self.layout_ruled_line(block, x, y) elif isinstance(block, ImageBlock): self.layout_image_block(block, x, y) elif isinstance(block, Canvas): self.layout_canvas(block, x, y) def layout_canvas(self, canvas, x, y): canvas.setParentItem(self) canvas.setPos(x, y) canvas.has_content = False oy = self.current_y for block, x, y in canvas.items: self.layout_block(block, x, oy+y) self.current_y = oy + canvas.max_y def layout_text_block(self, block, x, y): textwidth = block.bs.blockwidth - block.bs.sidemargin if block.max_y == 0 or not block.lines: # Empty block skipping self.is_full = False return line = block.peek() y += block.bs.topskip block_consumed = False while y + line.height <= self.max_y: block.commit() if isinstance(line, QGraphicsItem): line.setParentItem(self) line.setPos(x + line.getx(textwidth), y) y += line.height + line.line_space else: y += line.height if not block.has_content: y += block.bs.footskip block_consumed = True break else: line = block.peek() self.current_y = y self.is_full = not block_consumed def layout_ruled_line(self, rl, x, y): br = rl.boundingRect() rl.setParentItem(self) rl.setPos(x, y+1) self.current_y = y + br.height() + 1 self.is_full = y > self.max_y-5 rl.has_content = False def layout_image_block(self, ib, x, y): if self.current_y + ib.height > self.max_y-y and self.current_y < 5: self.is_full = True else: br = ib.boundingRect() max_height = min(br.height(), self.max_y-y) max_width = min(br.width(), self.max_x-x) if br.height() > max_height or br.width() > max_width: p = ib.pixmap() ib.setPixmap(p.scaled(max_width, max_height, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) br = ib.boundingRect() ib.setParentItem(self) ib.setPos(x, y) self.current_y = y + br.height() self.is_full = y > self.max_y-5 ib.has_content = False def search(self, phrase): matches = [] for child in self.children(): if hasattr(child, 'search'): res = child.search(phrase) if res: if isinstance(res, list): matches += res else: matches.append(res) return matches class Canvas(_Canvas, ContentObject): def __init__(self, font_loader, canvas, logger, opts, ruby_tags, link_activated, width=0, height=0): if hasattr(canvas, 'canvaswidth'): width, height = canvas.canvaswidth, canvas.canvasheight _Canvas.__init__(self, font_loader, logger, opts, width=width, height=height) self.block_id = canvas.id self.ruby_tags = ruby_tags self.link_activated = link_activated self.text_width = width fg = canvas.framecolor bg = canvas.bgcolor if not opts.visual_debug and canvas.framemode != 'none': self.setPen(Pen(fg, canvas.framewidth)) self.setBrush(QBrush(Color(bg))) self.items = [] for po in canvas: obj = po.object item = object_factory(self, obj, respect_max_y=True) if item: self.items.append((item, po.x, po.y)) def layout_block(self, block, x, y): block.reset() _Canvas.layout_block(self, block, x, y) class Header(Canvas): def __init__(self, font_loader, header, page_style, logger, opts, ruby_tags, link_activated): Canvas.__init__(self, font_loader, header, logger, opts, ruby_tags, link_activated, page_style.textwidth, page_style.headheight) if opts.visual_debug: self.setPen(QPen(Qt.blue, 1, Qt.DashLine)) class Footer(Canvas): def __init__(self, font_loader, footer, page_style, logger, opts, ruby_tags, link_activated): Canvas.__init__(self, font_loader, footer, logger, opts, ruby_tags, link_activated, page_style.textwidth, page_style.footheight) if opts.visual_debug: self.setPen(QPen(Qt.blue, 1, Qt.DashLine)) class Screen(_Canvas): def __init__(self, font_loader, chapter, odd, logger, opts, ruby_tags, link_activated): self.logger, self.opts = logger, opts page_style = chapter.style sidemargin = page_style.oddsidemargin if odd else page_style.evensidemargin width = 2*sidemargin + page_style.textwidth self.content_x = 0 + sidemargin self.text_width = page_style.textwidth self.header_y = page_style.topmargin self.text_y = self.header_y + page_style.headheight + page_style.headsep self.text_height = page_style.textheight self.footer_y = self.text_y + self.text_height + (page_style.footspace - page_style.footheight) _Canvas.__init__(self, font_loader, logger, opts, width=width, height=self.footer_y+page_style.footheight) if opts.visual_debug: self.setPen(QPen(Qt.red, 1, Qt.SolidLine)) header = footer = None if page_style.headheight > 0: try: header = chapter.oddheader if odd else chapter.evenheader except AttributeError: pass if page_style.footheight > 0: try: footer = chapter.oddfooter if odd else chapter.evenfooter except AttributeError: pass if header: header = Header(font_loader, header, page_style, logger, opts, ruby_tags, link_activated) self.layout_canvas(header, self.content_x, self.header_y) if footer: footer = Footer(font_loader, footer, page_style, logger, opts, ruby_tags, link_activated) self.layout_canvas(footer, self.content_x, self.header_y) self.page = None def set_page(self, page): if self.page is not None and self.page.scene(): self.scene().removeItem(self.page) self.page = page self.page.setPos(self.content_x, self.text_y) self.scene().addItem(self.page) def remove(self): if self.scene(): if self.page is not None and self.page.scene(): self.scene().removeItem(self.page) self.scene().removeItem(self) class Page(_Canvas): def __init__(self, font_loader, logger, opts, width, height): _Canvas.__init__(self, font_loader, logger, opts, width, height) if opts.visual_debug: self.setPen(QPen(Qt.cyan, 1, Qt.DashLine)) def id(self): for child in self.children(): if hasattr(child, 'block_id'): return child.block_id def add_block(self, block): self.layout_block(block, 0, self.current_y) class Chapter(object): num_of_pages = property(fget=lambda self: len(self.pages)) def __init__(self, oddscreen, evenscreen, pages, object_to_page_map): self.oddscreen, self.evenscreen, self.pages, self.object_to_page_map = \ oddscreen, evenscreen, pages, object_to_page_map def page_of_object(self, id): return self.object_to_page_map[id] def page(self, num): return self.pages[num-1] def screen(self, odd): return self.oddscreen if odd else self.evenscreen def search(self, phrase): pages = [] for i in range(len(self.pages)): matches = self.pages[i].search(phrase) if matches: pages.append([i, matches]) return pages class History(collections.deque): def __init__(self): collections.deque.__init__(self) self.pos = 0 def back(self): if self.pos - 1 < 0: return None self.pos -= 1 return self[self.pos] def forward(self): if self.pos + 1 >= len(self): return None self.pos += 1 return self[self.pos] def add(self, item): while len(self) > self.pos+1: self.pop() self.append(item) self.pos += 1 class Document(QGraphicsScene): num_of_pages = property(fget=lambda self: sum(self.chapter_layout)) def __init__(self, logger, opts): QGraphicsScene.__init__(self) self.logger, self.opts = logger, opts self.pages = [] self.chapters = [] self.chapter_layout = None self.current_screen = None self.current_page = 0 self.link_map = {} self.chapter_map = {} self.history = History() self.last_search = iter([]) if not opts.white_background: self.setBackgroundBrush(QBrush(QColor(0xee, 0xee, 0xee))) def page_of(self, oid): for chapter in self.chapters: if oid in chapter.object_to_page_map: return chapter.object_to_page_map[oid] def get_page_num(self, chapterid, objid): cnum = self.chapter_map[chapterid] page = self.chapters[cnum].object_to_page_map[objid] return sum(self.chapter_layout[:cnum])+page def add_to_history(self): page = self.chapter_page(self.current_page)[1] page_id = page.id() if page_id is not None: self.history.add(page_id) def link_activated(self, objid, on_creation=None): if on_creation is None: cid, oid = self.link_map[objid] self.add_to_history() page = self.get_page_num(cid, oid) self.show_page(page) else: jb = self.objects[objid] self.link_map[objid] = (jb.refpage, jb.refobj) def back(self): oid = self.history.back() if oid is not None: page = self.page_of(oid) self.show_page(page) def forward(self): oid = self.history.forward() if oid is not None: page = self.page_of(oid) self.show_page(page) def load_fonts(self, lrf, load_substitutions=True): font_map = {} for font in lrf.font_map: fdata = QByteArray(lrf.font_map[font].data) id = QFontDatabase.addApplicationFontFromData(fdata) font_map[font] = [str(i) for i in QFontDatabase.applicationFontFamilies(id)][0] if load_substitutions: from libprs500.ebooks.lrf.fonts.liberation import LiberationMono_BoldItalic QFontDatabase.addApplicationFontFromData(QByteArray(LiberationMono_BoldItalic.font_data)) from libprs500.ebooks.lrf.fonts.liberation import LiberationMono_Italic QFontDatabase.addApplicationFontFromData(QByteArray(LiberationMono_Italic.font_data)) from libprs500.ebooks.lrf.fonts.liberation import LiberationSerif_Bold QFontDatabase.addApplicationFontFromData(QByteArray(LiberationSerif_Bold.font_data)) from libprs500.ebooks.lrf.fonts.liberation import LiberationSans_BoldItalic QFontDatabase.addApplicationFontFromData(QByteArray(LiberationSans_BoldItalic.font_data)) from libprs500.ebooks.lrf.fonts.liberation import LiberationMono_Regular QFontDatabase.addApplicationFontFromData(QByteArray(LiberationMono_Regular.font_data)) from libprs500.ebooks.lrf.fonts.liberation import LiberationSans_Italic QFontDatabase.addApplicationFontFromData(QByteArray(LiberationSans_Italic.font_data)) from libprs500.ebooks.lrf.fonts.liberation import LiberationSerif_Regular QFontDatabase.addApplicationFontFromData(QByteArray(LiberationSerif_Regular.font_data)) from libprs500.ebooks.lrf.fonts.liberation import LiberationSerif_Italic QFontDatabase.addApplicationFontFromData(QByteArray(LiberationSerif_Italic.font_data)) from libprs500.ebooks.lrf.fonts.liberation import LiberationSans_Bold QFontDatabase.addApplicationFontFromData(QByteArray(LiberationSans_Bold.font_data)) from libprs500.ebooks.lrf.fonts.liberation import LiberationMono_Bold QFontDatabase.addApplicationFontFromData(QByteArray(LiberationMono_Bold.font_data)) from libprs500.ebooks.lrf.fonts.liberation import LiberationSerif_BoldItalic QFontDatabase.addApplicationFontFromData(QByteArray(LiberationSerif_BoldItalic.font_data)) from libprs500.ebooks.lrf.fonts.liberation import LiberationSans_Regular QFontDatabase.addApplicationFontFromData(QByteArray(LiberationSans_Regular.font_data)) self.font_loader = FontLoader(font_map, self.dpi) def render_chapter(self, chapter, lrf): oddscreen, evenscreen = Screen(self.font_loader, chapter, True, self.logger, self.opts, self.ruby_tags, self.link_activated), \ Screen(self.font_loader, chapter, False, self.logger, self.opts, self.ruby_tags, self.link_activated) pages = [] width, height = oddscreen.text_width, oddscreen.text_height current_page = Page(self.font_loader, self.logger, self.opts, width, height) object_to_page_map = {} for object in chapter: self.text_width = width block = object_factory(self, object) if block is None: continue while block.has_content: current_page.add_block(block) object_to_page_map[object.id] = len(pages) + 1 if current_page.is_full: pages.append(current_page) current_page = Page(self.font_loader, self.logger, self.opts, width, height) if current_page: pages.append(current_page) self.chapters.append(Chapter(oddscreen, evenscreen, pages, object_to_page_map)) self.chapter_map[chapter.id] = len(self.chapters)-1 def render(self, lrf, load_substitutions=True): self.dpi = lrf.device_info.dpi/10. self.ruby_tags = dict(**lrf.ruby_tags) self.load_fonts(lrf, load_substitutions) self.objects = lrf.objects num_chaps = 0 for pt in lrf.page_trees: for chapter in pt: num_chaps += 1 self.emit(SIGNAL('chapter_rendered(int)'), num_chaps) for pt in lrf.page_trees: for chapter in pt: self.render_chapter(chapter, lrf) self.emit(SIGNAL('chapter_rendered(int)'), -1) self.chapter_layout = [i.num_of_pages for i in self.chapters] self.objects = None def chapter_page(self, num): for chapter in self.chapters: if num <= chapter.num_of_pages: break num -= chapter.num_of_pages return chapter, chapter.page(num) def show_page(self, num): if num < 1 or num > self.num_of_pages or num == self.current_page: return odd = num%2 == 1 self.current_page = num chapter, page = self.chapter_page(num) screen = chapter.screen(odd) if self.current_screen is not None and self.current_screen is not screen: self.current_screen.remove() self.current_screen = screen if self.current_screen.scene() is None: self.addItem(self.current_screen) self.current_screen.set_page(page) self.emit(SIGNAL('page_changed(PyQt_PyObject)'), self.current_page) def next(self): self.next_by(1) def previous(self): self.previous_by(1) def next_by(self, num): self.show_page(self.current_page + num) def previous_by(self, num): self.show_page(self.current_page - num) def show_page_at_percent(self, p): num = self.num_of_pages*(p/100.) self.show_page(num) def search(self, phrase): if not phrase: return matches = [] for i in range(len(self.chapters)): cmatches = self.chapters[i].search(phrase) for match in cmatches: match[0] += sum(self.chapter_layout[:i])+1 matches += cmatches self.last_search = itertools.cycle(matches) self.next_match() def next_match(self): page_num = self.last_search.next()[0] if self.current_page == page_num: self.update() else: self.add_to_history() self.show_page(page_num)