From 64ed20a65d79dcc20eec86fc4567417580471742 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 22 Sep 2007 18:12:11 +0000 Subject: [PATCH] LRF viewer works on a few test files --- .../fonts/liberation/LiberationMono_Bold.pyc | Bin 61415 -> 61479 bytes .../liberation/LiberationMono_BoldItalic.pyc | Bin 65413 -> 65477 bytes .../liberation/LiberationMono_Italic.pyc | Bin 65309 -> 65373 bytes .../liberation/LiberationMono_Regular.pyc | Bin 61286 -> 61350 bytes .../fonts/liberation/LiberationSans_Bold.pyc | Bin 68599 -> 68663 bytes .../liberation/LiberationSans_BoldItalic.pyc | Bin 71105 -> 71169 bytes .../liberation/LiberationSans_Italic.pyc | Bin 71053 -> 71117 bytes .../liberation/LiberationSans_Regular.pyc | Bin 110802 -> 110866 bytes .../fonts/liberation/LiberationSerif_Bold.pyc | Bin 129556 -> 129620 bytes .../liberation/LiberationSerif_BoldItalic.pyc | Bin 100538 -> 100602 bytes .../liberation/LiberationSerif_Italic.pyc | Bin 100562 -> 100626 bytes .../liberation/LiberationSerif_Regular.pyc | Bin 156219 -> 156283 bytes src/libprs500/ebooks/lrf/html/convert_from.py | 6 +- src/libprs500/ebooks/lrf/parser.py | 4 +- src/libprs500/gui2/Makefile | 3 +- src/libprs500/gui2/images.qrc | 4 + src/libprs500/gui2/lrf_renderer/document.py | 773 ++++++++++++++---- src/libprs500/gui2/lrf_renderer/main.py | 136 ++- src/libprs500/gui2/lrf_renderer/main.ui | 262 +++++- src/libprs500/gui2/main.py | 15 +- src/libprs500/gui2/main_window.py | 30 + 21 files changed, 988 insertions(+), 245 deletions(-) create mode 100644 src/libprs500/gui2/main_window.py diff --git a/src/libprs500/ebooks/lrf/fonts/liberation/LiberationMono_Bold.pyc b/src/libprs500/ebooks/lrf/fonts/liberation/LiberationMono_Bold.pyc index e686837628dc793010359ca4dc608685025c3ea6..e1980b288bb96919cc0e6694e2e4c175e0b7474b 100644 GIT binary patch delta 194 zcmaEUpLzKMX5G!7c)2v)zPKebKmj9=b^zjHE+CP@zz_u_85x2#n1I}kdWrAr{WL&I zN;rW;T7F(hd`eEszD6u3nKTqEW&h*XC%a3=;&zXGoy(nuj$UXqlQ8jA- delta 112 zcmZ4ffcg1-X1&dyc)3{IzPKebKmj9^F6II$g!i@x6`)NJ$AN kkVwnVD~V4@EJ*|l1%bphfEYp)ivd}af4^5}EdhxE01sLevj6}9 diff --git a/src/libprs500/ebooks/lrf/fonts/liberation/LiberationMono_BoldItalic.pyc b/src/libprs500/ebooks/lrf/fonts/liberation/LiberationMono_BoldItalic.pyc index 10533d8f0189f369cfdcf9df5d38df4b5f7ef243..4f201a88cdbe052545f09585876887d24b3ce035 100644 GIT binary patch delta 194 zcmZqu&wTVhv+m|kyj+@YU)+)zpnwrbI{ej10jVOhE2Ny`TT;{WL&I zN;rW;T7F(hd`eEszD6u3nKTqEW&h*XC%a3=;&zYS1Uz)WTWFG)1MK!?y delta 112 zcmX^5pSkrvv)<-Uyj(19U)+)zpnwrd7jpp_DU1wJK$3wWSc3`3_1mcX^Pi3eNJ$AN kkVwnVD~V4@EJ*|l1%bphfEYp)%K%xEC;!)FEdhxE05B&N2><{9 diff --git a/src/libprs500/ebooks/lrf/fonts/liberation/LiberationMono_Italic.pyc b/src/libprs500/ebooks/lrf/fonts/liberation/LiberationMono_Italic.pyc index 43724eb673249b7ee9c3f1e89c0a1e40f87f1dd8..bb7d536f851535a9a66735af7df29aff78a3b2ad 100644 GIT binary patch delta 176 zcmbRHkNNIDX5G!7c)2v)zPKebKmj9=b^zjHE+CP@zz_u_85x2#n1I}kdQ<=9`e}fa zlyCxxwEVo1_>{zwL=7N^0YMbU0=fDb`MIh3+4*IeDf;F4McMi}nMnmj#ij-Z`o%@b oNW9df{QT@<{hXpSeUO1bR%TLaQDRAEexCm1_y06mi$SIX0B+ze?f?J) delta 112 zcmccnk9qDtX1&dyc)3{IzPKebKmj9^F6II$g#N>OUO~kdhKk kAd!}zR}!C+Sds`93Id6105OCpmH@IQ>;KneEdhxE04eAcX8-^I diff --git a/src/libprs500/ebooks/lrf/fonts/liberation/LiberationMono_Regular.pyc b/src/libprs500/ebooks/lrf/fonts/liberation/LiberationMono_Regular.pyc index f610f0e3a9a95b7d2c34192e190d1c8cad33cdee..8fd6ef18f4c92e609014828c646cce80ca68a861 100644 GIT binary patch delta 194 zcmaEMk9paBX5G!7c)7IPzPKebKmj9=b^zjHE+CP@zz_u_85x2#n1I}kdUxN|`)Pob zlyCxxwEVo1_>{zwL=7N^0YMbU0lE4a`MIh3+4*IeDf;F4McMi}nMnmj#ij-Z`o%@b yNW9df{QT@<{hXpSeUO1bR%TLaQDRAEexAM$oavjNmmeRLnm*a{y##A9$UXoN9W`bE delta 112 zcmZ2>pZVE6X1&dyc)3{IzPKebKmj9^F6II$g$&?mHa~kdhKk kAd!}zR}!C+Sds`93Id6105OCpmISgUm%rCyEdhxE03@LltN;K2 diff --git a/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSans_Bold.pyc b/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSans_Bold.pyc index f72124a0e882f4f333f478fff78225f18361c2b1..319f18fcdd0f1fe0af9ae148dead625a7a80594d 100644 GIT binary patch delta 198 zcmex9on`wB7TwLCc)7IPzPKebKmj9=b^zjHE+CP@zz_u_85x2#n1I|yy{&qTh1~Ui z8X#3AoIoNiKd&S{C9xz?1IS@O5XI3zu6{;-ZmNEEepzOUetCXTwth}#QbAF%seyrh zaZxf7FEuGYKf72zrzlMyWFU~0nUq?TSdy8ar|$!21}Ekf$2;ZcOux&`D9Tz4vJe2g C?=%Yl delta 116 zcmdl!gXQ~l7QM}%c)3{IzPKebKmj9^F6II({8eRhO}lTSo(= ns)Q3rq~+(8#HS>dB!Y#4K;jxe3?YiefUN1ej10jVOh9g<-c~(EQ_);M z4Uno5P9Tw%pH~u}l30?c0pu_sh~h*bS3e^^H&s77zbrFFzdXMvTR$f=si3IX)WAT$ rxF{KkmztEHpIxkyj(19U)+)zpnwrd7jpp_DU1wJK$3wWSc3`3^=s7Ks>^69s-po? nRl*4*((?04;!_e!62U@2AaM;Kh7iRvK-Tp0qKvw%B_Kfn+Fla5 diff --git a/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSans_Italic.pyc b/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSans_Italic.pyc index b32ad1bcfeb880e52dfc5356751977acc3df6689..0b4254477125401142d8a2fc928ee05fab2a1211 100644 GIT binary patch delta 180 zcmeBO&2n}%i|*!6yj)stU)+)zpnwrbI{ej10jVOh9g<-c~(ER?%EP z4Uno5P9Tw%pH~u}l30?c0pu_sh~iivS3e^^H&s77zbrFFzdXMvTR$f=si3IX)WAT$ rxF{KkmztEHpIxkyj(19U)+)zpnwrd7jpp_DU1wJK$3wWSc3`3^=s7Ks>{eKs-po? nRl*4*((?04;!_e!62U@2AaM;Kh7iRPK-Tp6qKulXB_Kfn*iRB@ diff --git a/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSans_Regular.pyc b/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSans_Regular.pyc index fb941005a2f52be1014428b5f581af9691ea3b5f..a22270372cb25a115a57793bc7a7908e99e6fd70 100644 GIT binary patch delta 198 zcmccgkZsZ
sic)7IPzPKebKmj9=b^zjHE+CP@zz_u_85x2#n1I|yy{&qTo*U}@ zG(f6KIDtf3eqKp@N@7W(29U#mAd2IFT>Xsv+*JMS{Ibjx{qp>xZ2g?fq=KSiQv(D2 z;-X|EUTRW)es-~bPEndZ$Uq<~Gbyzwu_QA;Pu~a53{K1|jt@#rpT2biqXcU)$U*=@ C8#P)0 delta 116 zcmbRAi0#rtHoeWCc)3{IzPKebKmj9^F6II({8eRhQ9ogN_DB oRS74MNXySFiBCx^NdyZ8fy6a{7(x_F0$I}^Z(!77EddDv07yy{qW}N^ diff --git a/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSerif_Bold.pyc b/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSerif_Bold.pyc index 1f3397c97ababee2783e9bb1a6375c4c50addeba..a443cccb080ffcd937168c79f3493107447c5084 100644 GIT binary patch delta 198 zcmbR8h5gDGcHPaNc)7IQzPKebKmj9=b^zjHE+CP@zz_u_85x2#n1I|yy{&qTy+7;y zG(f6KIDtf3eqKp@N@7W(29U#mAc|vvT>Xsv+*JMS{Ibjx{qp>xZ2g?fq=KSiQv(D2 z;-X|EUTRW)es-~bPEndZ$Uq<~Gbyzwu_QA;Pu~a53{EY|OpABQpZ@bFqZn&3$U*?L CQ#O+T delta 116 zcmcceg?-8wcD>D?c)3{IzPKebKmj9^F6II({8eRhO~%r;Y|l nRS74MNXySFiBCx^NdyZ8fy6a{7(x__16kA6e=%yXmVg8ScG?u> diff --git a/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSerif_BoldItalic.pyc b/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSerif_BoldItalic.pyc index da3fefd155258d70b6b4c660fde255c32ee0f330..b5ddb11409e57691d64cb5b5ea06981ca31b6d31 100644 GIT binary patch delta 198 zcmdlrk?q$+Hr>sic)7IQzPKebKmj9=b^zjHE+CP@zz_u_85x2#n1I|yy{&qTx*hd? z8X#3AoIoNiKd&S{C9xz?1IS@O5XDJAu6{;-ZmNEEepzOUetCXTwth}#QbAF%seyrh zaZxf7FEuGYKf72zrzlMyWFU~0nUq?TSdy8ar|$!22B#Kfro}tuPoLSrD8pI|vJe2? C2{eHK delta 116 zcmex0k!{yRHoeWCc)3{IzPKebKmj9^F6II({8eRhLn>Lq`Lo ns)Q3rq~+(8#HS>dB!Y#4K;jxe3?YhTfvoArI~es?OF)7E6bBR1 diff --git a/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSerif_Italic.pyc b/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSerif_Italic.pyc index 9acfd54a8207919c2b67d015aab416873e8764a9..c363d0ce52e9c584e700c7b9187a353f3e864f80 100644 GIT binary patch delta 198 zcmcaKk!{i>Hr>sic)7IQzPKebKmj9=b^zjHE+CP@zz_u_85x2#n1I|yy{&qTo*ngm z8X#3AoIoNiKd&S{C9xz?1IS@O5XEsou6{;-ZmNEEepzOUetCXTwth}#QbAF%seyrh zaZxf7FEuGYKf72zrzlMyWFU~0nUq?TSdy8ar|$!22B#Kfrp0@fOyAnUD8X6`vJe2V CiZpHj delta 116 zcmbO({8eRhQATLq`Lo ns)Q3rq~+(8#HS>dB!Y#4K;jxe3?Ygofvo9|I~cWCOF)7E_WKjF diff --git a/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSerif_Regular.pyc b/src/libprs500/ebooks/lrf/fonts/liberation/LiberationSerif_Regular.pyc index ccf3b1a48b2fc01b856dad7094a035247c5bc616..4e4a6ffe48b321a0a7bf5027aae44551f178fe8a 100644 GIT binary patch delta 201 zcmdmeg!A_iPTkF)c)7IQzPKebKmj9=b^zjHE+CP@zz_u_85x2#n1I|yy;eQOt$Iuw z6YKpnKnhDZfkawGyvdk3y^8BJ~{hZ9Cf}&zm z0|WiyqGTjqYEpiFcCmg=QJOx;Kp-nKDYYoEBr`ux-v`bNPA$qziw{bjE}O(8$yyAu F5CG*FHDCY$ delta 119 zcmex;gmd>1PQA^ac)3{IzPKebKmj9^F6II({8;s>`@lmuX|7 qjs{3!2`7+9%g-x`Pf09E1PcX$#5I5zLKI5@S<}6fn6z0-K!O0D85F7j diff --git a/src/libprs500/ebooks/lrf/html/convert_from.py b/src/libprs500/ebooks/lrf/html/convert_from.py index dd9f1677e7..68e2ce03cc 100644 --- a/src/libprs500/ebooks/lrf/html/convert_from.py +++ b/src/libprs500/ebooks/lrf/html/convert_from.py @@ -813,9 +813,9 @@ class HTMLConverter(object): height=height)) left = int(floor((pwidth - width)/2.)) self.current_page.contents[-1].put_object( - ImageBlock(self.images[path], xsize=pwidth, - ysize=pheight, x1=pwidth, y1=pheight, - blockwidth=pwidth, blockheight=pheight), + ImageBlock(self.images[path], xsize=width, + ysize=height, x1=width, y1=height, + blockwidth=width, blockheight=height), left, 0) def process_page_breaks(self, tag, tagname, tag_css): diff --git a/src/libprs500/ebooks/lrf/parser.py b/src/libprs500/ebooks/lrf/parser.py index 0e2274939c..2ea97d2c3e 100644 --- a/src/libprs500/ebooks/lrf/parser.py +++ b/src/libprs500/ebooks/lrf/parser.py @@ -93,8 +93,8 @@ class LRFDocument(LRFMetaFile): bookinfo += u'%s\n\n\n'%(self.metadata.free_text,) th = self.doc_info.thumbnail if th: - bookinfo += u'\n'%(self.metadata.title+'_thumbnail.'+self.doc_info.thumbail_extension(),) - open(self.metadata.title+'_thumbnail.'+self.doc_info.thumbail_extension(), 'wb').write(th) + bookinfo += u'\n'%(self.metadata.title+'_thumbnail.'+self.doc_info.thumbnail_extension,) + open(self.metadata.title+'_thumbnail.'+self.doc_info.thumbnail_extension, 'wb').write(th) bookinfo += u'%s\n'%(self.doc_info.language,) bookinfo += u'%s\n'%(self.doc_info.creator,) bookinfo += u'%s\n'%(self.doc_info.producer,) diff --git a/src/libprs500/gui2/Makefile b/src/libprs500/gui2/Makefile index 8c0ae5fd7d..cd17268a4e 100644 --- a/src/libprs500/gui2/Makefile +++ b/src/libprs500/gui2/Makefile @@ -4,7 +4,8 @@ UI = main_ui.py dialogs/metadata_single_ui.py dialogs/metadata_bulk_ui.py dialog RC = images_rc.pyc %_ui.py : %.ui - pyuic4 $< | sed "s/import images_rc/from libprs500.gui2 import images_rc/" > $@ + pyuic4 $< | sed "s/import images_rc/from libprs500.gui2 import images_rc/" | \ + sed "s/from library import/from libprs500.gui2.library import/"> $@ %_rc.pyc : %.qrc % pyrcc4 $< > $*_rc.py diff --git a/src/libprs500/gui2/images.qrc b/src/libprs500/gui2/images.qrc index 151d2d68e4..5e4a84cced 100644 --- a/src/libprs500/gui2/images.qrc +++ b/src/libprs500/gui2/images.qrc @@ -1,5 +1,6 @@ + images/back.svg images/book.svg images/chapters.svg images/clear_left.svg @@ -9,6 +10,7 @@ images/dialog_warning.svg images/document_open.svg images/edit_input.svg + images/forward.svg images/jobs-animated.mng images/jobs.svg images/library.png @@ -36,8 +38,10 @@ images/news/bbc.png images/news/newsweek.png images/news/nytimes.png + images/next.svg images/page.svg images/plus.svg + images/previous.svg images/reader.svg images/save.svg images/sd.svg diff --git a/src/libprs500/gui2/lrf_renderer/document.py b/src/libprs500/gui2/lrf_renderer/document.py index 861b200021..6e94a1a225 100644 --- a/src/libprs500/gui2/lrf_renderer/document.py +++ b/src/libprs500/gui2/lrf_renderer/document.py @@ -14,16 +14,22 @@ ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''''' -import operator, collections, copy, re +import operator, collections, copy, re, sys -from PyQt4.QtCore import Qt, QByteArray +from PyQt4.QtCore import Qt, QByteArray, SIGNAL, QVariant, QUrl from PyQt4.QtGui import QGraphicsRectItem, QGraphicsScene, QPen, \ - QBrush, QColor, QGraphicsSimpleTextItem, QFontDatabase, \ - QFont, QGraphicsItem + QBrush, QColor, QGraphicsTextItem, QFontDatabase, \ + QFont, QGraphicsItem, QGraphicsLineItem, QPixmap, \ + QGraphicsPixmapItem, QTextCharFormat, QTextFrameFormat, \ + QTextBlockFormat, QTextCursor, QTextImageFormat, \ + QTextDocument from libprs500.ebooks.lrf.fonts import FONT_MAP from libprs500.gui2 import qstring_to_unicode from libprs500.ebooks.hyphenate import hyphenate_word +from libprs500.ebooks.BeautifulSoup import Tag +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): @@ -38,124 +44,273 @@ WEIGHT_MAP = lambda wt : int((wt/10.)-1) class FontLoader(object): + font_map = { + 'Swis721 BT Roman' : 'Liberation Sans', + 'Dutch801 Rm BT Roman' : 'Liberation Serif', + 'Courier10 BT Roman' : 'Liberation Mono', + } + def __init__(self, font_map, dpi): self.face_map = {} self.cache = {} self.dpi = dpi - - for font in font_map: - weight = WEIGHT_MAP(800) if 'Bold' in font else WEIGHT_MAP(400) - style = QFont.StyleItalic if 'Italic' in font else QFont.StyleNormal - self.face_map[font] = [font_map[font], weight, style] + self.face_map = font_map - def font(self, text_style, face=None, size=None, weight=None, style=QFont.StyleNormal): - face = self.face_map[text_style.fontfacename if face is None else face] - sz = text_style.fontsize if size is None else size - wt = text_style.fontweight if weight is None else weight - face[1], face[2] = wt, style - font = tuple(face) + (sz,) + def font(self, text_style): + device_font = text_style.fontfacename in FONT_MAP + if device_font: + face = self.font_map[text_style.fontfacename] + else: + face = self.face_map[text_style.fontfacename] + + sz = text_style.fontsize + wt = text_style.fontweight + style = text_style.fontstyle + font = (face, wt, style, sz,) if font in self.cache: - return self.cache[font] - italic = font[2] == QFont.StyleItalic - qfont = QFont(font[0], font[3], font[1], italic) - qfont.setPixelSize(font[3]) - self.cache[font] = qfont + rfont = self.cache[font] + else: + italic = font[2] == QFont.StyleItalic + rfont = QFont(font[0], font[3], font[1], italic) + rfont.setPixelSize(font[3]) + rfont.setBold(wt>=69) + self.cache[font] = rfont + qfont = rfont + if text_style.underline or text_style.overline: + qfont = QFont(rfont) + qfont.setOverline(text_style.overline) + qfont.setUnderline(text_style.underline) return qfont class ParSkip(object): def __init__(self, parskip): self.height = parskip + def __str__(self): + return 'Parskip: '+str(self.height) + +class PixmapItem(QGraphicsPixmapItem): + def __init__(self, data, encoding, x0, y0, x1, y1, xsize, ysize): + p = QPixmap() + p.loadFromData(data, encoding, Qt.AutoColor) + w, h = p.width(), p.height() + p = p.copy(x0, y0, min(w, x1-x0), min(h, y1-y0)) + if p.width() != xsize or p.height() != ysize: + p = p.scaled(xsize, ysize, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) + QGraphicsPixmapItem.__init__(self, p) + self.height, self.width = ysize, xsize + self.setTransformationMode(Qt.SmoothTransformation) + self.setShapeMode(QGraphicsPixmapItem.BoundingRectShape) + + +class Plot(PixmapItem): - + def __init__(self, plot, dpi): + img = plot.refobj + xsize, ysize = dpi*plot.attrs['xsize']/720., dpi*plot.attrs['xsize']/720. + x0, y0, x1, y1 = img.x0, img.y0, img.x1, img.y1 + data, encoding = img.data, img.encoding + PixmapItem.__init__(self, data, encoding, x0, y0, x1, y1, xsize, ysize) + + class Line(QGraphicsRectItem): whitespace = re.compile(r'\s+') + no_pen = QPen(Qt.NoPen) + inactive_brush = QBrush(QColor(0x00, 0x00, 0x00, 0x09)) + active_brush = QBrush(QColor(0x00, 0x00, 0x00, 0x59)) - def __init__(self, offset, linespace, linelength, align, hyphenate): + line_map = { + 'none' : QTextCharFormat.NoUnderline, + 'solid' : QTextCharFormat.SingleUnderline, + 'dotted' : QTextCharFormat.DotLine, + 'dashed' : QTextCharFormat.DashUnderline, + 'double' : QTextCharFormat.WaveUnderline, + } + + + def __init__(self, offset, linespace, linelength, align, hyphenate, ts, block_id): QGraphicsRectItem.__init__(self, 0, 0, 0, 0) self.offset, self.line_space, self.line_length = offset, linespace, linelength - self.linepos = 0 self.align = align - self.hyphenate = hyphenate + self.do_hyphenation = hyphenate + self.setPen(self.__class__.no_pen) + self.is_empty = True + self.highlight_rect = None + self.cursor = None + self.item = None + self.plot_counter = 0 + self.create_text_item(ts) + self.block_id = block_id - def hyphenate_word(self, word): - if self.hyphenate: - tokens = hyphenate_word(word) - for i in range(len(tokens)-2, -1, -1): - part = ''.join(tokens[0:i+1]) - rword = QGraphicsSimpleTextItem(part+'-') - length = rword.boundingRect().width() - if length <= self.line_length - self.linepos: - return len(part), rword, length - if self.linepos == 0: - return self.force_hyphenate_word(word) - return 0, None, 0 - - def force_hyphenate_word(self, word): - for i in range(len(word)-5, 0, -5): - part = word[:i] - rword = QGraphicsSimpleTextItem(part+'-') - length = rword.boundingRect().width() - if length <= self.line_length - self.linepos: - return len(part), rword, length - - def populate(self, phrase, font, wordspace): + def hoverEnterEvent(self, event): + if self.highlight_rect is not None: + self.highlight_rect.setBrush(self.__class__.active_brush) + + def hoverLeaveEvent(self, event): + if self.highlight_rect is not None: + self.highlight_rect.setBrush(self.__class__.inactive_brush) + + def mousePressEvent(self, event): + if self.highlight_rect is not None: + self.hoverLeaveEvent(None) + self.link[1](self.link[0]) + + def create_link(self, pos, in_link): + if not self.acceptsHoverEvents(): + self.setAcceptsHoverEvents(True) + self.highlight_rect = QGraphicsRectItem(pos, 0, 0, 0, self) + self.highlight_rect.setCursor(Qt.PointingHandCursor) + self.link = in_link + self.link_end = sys.maxint + + def end_link(self): + self.link_end = self.item.boundingRect().width() - self.highlight_rect.boundingRect().x() + + def add_plot(self, plot, ts, in_link): + label='plot%d'%(self.plot_counter,) + self.plot_counter += 1 + pos = self.item.boundingRect().width() + self.item.document().addResource(QTextDocument.ImageResource, QUrl(label), + QVariant(plot.pixmap())) + qif = QTextImageFormat() + qif.setHeight(plot.height) + qif.setWidth(plot.width) + qif.setName(label) + self.cursor.insertImage(qif, QTextFrameFormat.InFlow) + if in_link: + self.create_link(pos, in_link) + + + def can_add_plot(self, plot): + pos = self.item.boundingRect().width() if self.item is not None else 0 + return self.line_length - pos >= plot.width + + def create_text_item(self, ts): + self.item = QGraphicsTextItem(self) + self.cursor = QTextCursor(self.item.document()) + f = self.cursor.currentFrame() + ff = QTextFrameFormat() + ff.setBorder(0) + ff.setPadding(0) + ff.setMargin(0) + f.setFrameFormat(ff) + bf = QTextBlockFormat() + bf.setTopMargin(0) + bf.setRightMargin(0) + bf.setBottomMargin(0) + bf.setRightMargin(0) + bf.setNonBreakableLines(True) + self.cursor.setBlockFormat(bf) + + + def build_char_format(self, ts): + tcf = QTextCharFormat() + tcf.setFont(ts.font) + tcf.setVerticalAlignment(ts.valign) + tcf.setForeground(ts.textcolor) + tcf.setUnderlineStyle(self.line_map[ts.line_style]) + return tcf + + def populate(self, phrase, ts, wordspace, in_link): phrase_pos = 0 processed = False - for match in self.__class__.whitespace.finditer(phrase): + matches = self.__class__.whitespace.finditer(phrase) + tcf = self.build_char_format(ts) + if in_link: + start = self.item.boundingRect().width() + for match in matches: processed = True left, right = match.span() - if left == 0: - self.linepos += right*wordspace - phrase_pos = right - continue - word = phrase[phrase_pos:left] - rword = QGraphicsSimpleTextItem(word, self) - rword.setFont(font) - length = rword.boundingRect().width() - if length > self.line_length - self.linepos: - break_at, rword, length = self.hyphenate(word) - if break_at == 0: - return phrase_pos - phrase_pos += break_at - else: - phrase_pos = right - rword.setParentItem(self) - rword.setPos(self.linepos, 0) - self.linepos += length + (right-left)*wordspace - - if self.line_length - self.linepos < 15: # Efficiency to prevent excessive hyphenation - return phrase_pos - + if wordspace == 0: + right = left + word = phrase[phrase_pos:right] + self.cursor.insertText(word, tcf) + if self.item.boundingRect().width() > self.line_length: + self.cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor, + right-left) + self.cursor.removeSelectedText() + if self.item.boundingRect().width() <= self.line_length: + if in_link: self.create_link(start, in_link) + return right, True + self.cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor, + left-phrase_pos) + self.cursor.removeSelectedText() + if self.do_hyphenation: + tokens = hyphenate_word(word) + for i in range(len(tokens)-2, -1, -1): + part = ''.join(tokens[0:i+1]) + self.cursor.insertText(part+'-', tcf) + if self.item.boundingRect().width() <= self.line_length: + if in_link: self.create_link(start, in_link) + return phrase_pos + len(part), True + self.cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor, + len(part)+1) + self.cursor.removeSelectedText() + if self.cursor.position() < 1: # Force hyphenation as word is longer than line + for i in range(len(word)-5, 0, -5): + part = word[:i] + self.cursor.insertText(part+'-', tcf) + if self.item.boundingRect().width() <= self.line_length: + if in_link: self.create_link(start, in_link) + return phrase_pos + len(part), True + self.cursor.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor, + len(part)+1) + self.cursor.removeSelectedText() + return phrase_pos, True + + if in_link: self.create_link(start, in_link) + phrase_pos = right + if not processed: - self.populate(phrase+' ', font, 0) + return self.populate(phrase+' ', ts, 0, in_link) + + return phrase_pos, False + + def finalize(self, wordspace, vdebug): crect = self.childrenBoundingRect() - self.width = self.linepos - wordspace + self.width = crect.width() - wordspace self.height = crect.height() + self.line_space self.setRect(crect) + if vdebug: self.setPen(QPen(Qt.yellow, 1, Qt.DotLine)) + if self.highlight_rect is not None: + x = self.highlight_rect.boundingRect().x() + if self.link_end == sys.maxint: + self.link_end = crect.width()-x + self.highlight_rect.setRect(crect) + erect = self.highlight_rect.boundingRect() + erect.setX(x) + erect.setWidth(self.link_end) + self.highlight_rect.setRect(erect) + self.highlight_rect.setBrush(self.__class__.inactive_brush) + self.highlight_rect.setZValue(-1) + self.highlight_rect.setPen(self.__class__.no_pen) + + return self.height def getx(self, textwidth): if self.align == 'head': return self.offset if self.align == 'foot': return textwidth - self.width - if self.align == 'center': + if self.align == 'center': return (textwidth-self.width)/2. def __unicode__(self): s = u'' for word in self.children(): - s += qstring_to_unicode(word.text()) + ' ' - return + if not hasattr(word, 'toPlainText'): + continue + s += qstring_to_unicode(word.toPlainText()) + return s def __str__(self): return unicode(self).encode('utf-8') - + class ContentObject(object): has_content = True @@ -181,6 +336,9 @@ class Style(object): for attr in kwds: setattr(self, attr, self.__class__.map[attr](kwds[attr], self.fdpi)) + def copy(self): + return copy.copy(self) + class TextStyle(Style): @@ -201,10 +359,23 @@ class TextStyle(Style): linecolor = COLOR, ) - def __init__(self, style, dpi, ruby_tags): - Style.__init__(self, style, dpi) + def __init__(self, style, font_loader, ruby_tags): + self.font_loader = font_loader + self.fontstyle = QFont.StyleNormal + self.valign = QTextCharFormat.AlignBottom + self.underline = False + self.overline = False + self.line_style = 'none' + Style.__init__(self, style, font_loader.dpi) for attr in ruby_tags: setattr(self, attr, ruby_tags[attr]) + self.font = self.font_loader.font(self) + + + def update(self, *args, **kwds): + Style.update(self, *args, **kwds) + self.font = self.font_loader.font(self) + class BlockStyle(Style): map = collections.defaultdict(lambda : NULL, @@ -212,31 +383,48 @@ class BlockStyle(Style): framecolor = COLOR, ) - + class TextBlock(ContentObject): - has_content = property(fget=lambda self: self.peek_index < len(self.lines)-2) - - def __init__(self, tb, font_loader, logger, opts, ruby_tags, parent=None, x=0, y=0): + has_content = property(fget=lambda self: self.peek_index < len(self.lines)-1) + XML_ENTITIES = dict(zip(Tag.XML_SPECIAL_CHARS_TO_ENTITIES.values(), Tag.XML_SPECIAL_CHARS_TO_ENTITIES.keys())) + XML_ENTITIES["quot"] = '"' + + class HeightExceeded(Exception): + pass + + + def __init__(self, tb, font_loader, respect_max_y, text_width, logger, + opts, ruby_tags, link_activated, + parent=None, x=0, y=0): ContentObject.__init__(self) + self.block_id = tb.id self.bs, self.ts = BlockStyle(tb.style, font_loader.dpi), \ - TextStyle(tb.textstyle, font_loader.dpi, ruby_tags) + TextStyle(tb.textstyle, font_loader, ruby_tags) self.bs.update(tb.attrs) self.ts.update(tb.attrs) self.lines = [] - self.line_length = self.bs.blockwidth - 2*self.bs.sidemargin + self.line_length = min(self.bs.blockwidth, text_width) + self.line_length -= 2*self.bs.sidemargin self.line_offset = self.bs.sidemargin self.first_line = True - self.current_style = copy.copy(self.ts) + self.current_style = self.ts.copy() self.current_line = None self.font_loader, self.logger, self.opts = font_loader, logger, opts - self.current_font = font_loader.font(self.current_style) - self.create_lines(tb.content) - if self.current_line: - self.end_line() + self.in_link = False + self.link_activated = link_activated + self.max_y = self.bs.blockheight if (respect_max_y or self.bs.blockrule.lower() in ('vert-fixed', 'block-fixed')) else sys.maxint + self.height = 0 + try: + if self.max_y > 0: + self.populate(tb.content) + self.end_line() + except TextBlock.HeightExceeded: + logger.warning('TextBlock height exceeded, truncating.') self.peek_index = -1 + def peek(self): return self.lines[self.peek_index+1] @@ -246,32 +434,82 @@ class TextBlock(ContentObject): def reset(self): self.peek_index = -1 - def create_lines(self, tb): - for i in tb: - if i.name == 'CR': - self.lines.append(ParSkip(self.current_style.parskip)) - self.first_line = True - elif i.name == 'P': - self.process_container(i) - - def process_container(self, c): - for i in c: + def end_link(self): + self.link_activated(self.in_link[0], on_creation=True) + self.in_link = False + + def populate(self, tb): + self.create_line() + open_containers = collections.deque() + self.in_para = False + for i in tb.content: if isinstance(i, basestring): self.process_text(i) + elif i is None: + if len(open_containers) > 0: + for a, b in open_containers.pop(): + if callable(a): + a(*b) + else: + setattr(self, a, b) + elif i.name == 'P': + open_containers.append((('in_para', False),)) + self.in_para = True elif i.name == 'CR': - self.end_line() + if self.in_para: + self.end_line() + self.create_line() + else: + self.end_line() + delta = self.current_style.parskip + if isinstance(self.lines[-1], ParSkip): + delta += self.current_style.baselineskip + self.lines.append(ParSkip(delta)) + self.first_line = True + elif i.name == 'Span': + open_containers.append((('current_style', self.current_style.copy()),)) + self.current_style.update(i.attrs) + elif i.name == 'CharButton': + open_containers.append(((self.end_link, []),)) + self.in_link = (i.attrs['refobj'], self.link_activated) + elif i.name == 'Italic': + open_containers.append((('current_style', self.current_style.copy()),)) + self.current_style.update(fontstyle=QFont.StyleItalic) elif i.name == 'Plot': - pass #TODO: Plot + plot = Plot(i, self.font_loader.dpi) + if self.current_line is None: + self.create_line() + if not self.current_line.can_add_plot(plot): + self.end_line() + self.create_line() + self.current_line.add_plot(plot, self.current_style, self.in_link) + elif i.name == 'Sup': + open_containers.append((('current_style', self.current_style.copy()),)) + self.current_style.valign=QTextCharFormat.AlignSuperScript + elif i.name == 'Sub': + open_containers.append((('current_style', self.current_style.copy()),)) + self.current_style.valign=QTextCharFormat.AlignSubScript + elif i.name == 'EmpLine': + open_containers.append((('current_style', self.current_style.copy()),)) + if i.attrs['emplineposition'] == 'before': + self.current_style.overline = True + if i.attrs['emplineposition'] == 'after': + self.current_style.underline = True + self.current_style.update(line_type=i.attrs['emplinetype']) else: - self.process_container(i) + self.logger.warning('Unhandled TextTag %s'%(i.name,)) + if not i.self_closing: + open_containers.append([]) def __iter__(self): for line in self.lines: yield line def end_line(self): if self.current_line is not None: - self.lines.append(self.current_line) - self.current_line.finalize(self.current_style.wordspace, self.opts.visual_debug) + self.height += self.current_line.finalize(self.current_style.wordspace, self.opts.visual_debug) + if self.height > self.max_y+10: + raise TextBlock.HeightExceeded + self.lines.append(self.current_line) self.current_line = None def create_line(self): @@ -281,35 +519,65 @@ class TextBlock(ContentObject): line_length -= self.current_style.parindent line_offset += self.current_style.parindent self.current_line = Line(line_offset, self.current_style.linespace, - line_length, self.current_style.align, self.opts.hyphenate) + line_length, self.current_style.align, + self.opts.hyphenate, self.current_style, self.block_id) self.first_line = False def process_text(self, raw): - if len(raw) == 0: - return - while True: + for ent, rep in TextBlock.XML_ENTITIES.items(): + raw = raw.replace(u'&%s;'%ent, rep) + while len(raw) > 0: if self.current_line is None: self.create_line() - pos = self.current_line.populate(raw, self.current_font.font, - self.current_style.wordspace) - if pos >= len(raw): - self.end_line() - break + pos, line_filled = self.current_line.populate(raw, self.current_style, + self.current_style.wordspace, self.in_link) raw = raw[pos:] - + if line_filled: + self.end_line() + def __unicode__(self): return u'\n'.join(unicode(l) for l in self.lines) def __str__(self): - return unicode(self).encode('utf-8') + + return '\n'+unicode(self).encode('utf-8')+'\n' + +class RuledLine(QGraphicsLineItem, ContentObject): -def object_factory(container, obj): + 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, container.logger, - container.opts, container.ruby_tags) + 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): @@ -317,7 +585,7 @@ 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 = 0, height + self.current_y, self.max_y, self.max_x = 0, height, width self.is_full = False pen = QPen() pen.setStyle(Qt.NoPen) @@ -326,18 +594,36 @@ class _Canvas(QGraphicsRectItem): 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() - self.max_y = self.boundingRect().height() 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(line.getx(textwidth), y) + line.setPos(x + line.getx(textwidth), y) y += line.height if not block.has_content: y += block.bs.footskip @@ -348,49 +634,70 @@ class _Canvas(QGraphicsRectItem): 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() + 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 + -class Canvas(_Canvas): +class Canvas(_Canvas, ContentObject): - def __init__(self, font_loader, canvas, logger, opts, width=0, height=0): + 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) - if item: - self.layout_block(item, po.x, po.y) + 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): - Canvas.__init__(self, font_loader, header, logger, opts, + 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): - Canvas.__init__(self, font_loader, footer, logger, opts, + 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): + 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 @@ -412,22 +719,26 @@ class Screen(_Canvas): if page_style.footheight > 0: footer = chapter.oddfooter if odd else chapter.evenfooter if header: - header = Header(font_loader, header, page_style, logger, opts) - header.setParentItem(self) - header.setPos(self.content_x, self.header_y) + 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) - footer.setParentItem(self) - footer.setPos(self.content_x, self.footer_y) + 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: - self.scene().removeItem(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): @@ -436,10 +747,14 @@ class Page(_Canvas): _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) + self.layout_block(block, 0, self.current_y) class Chapter(object): @@ -459,94 +774,194 @@ class Chapter(object): def screen(self, odd): return self.oddscreen if odd else self.evenscreen +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, lrf, logger, opts): + def __init__(self, logger, opts): QGraphicsScene.__init__(self) - #opts.hyphenate = opts.hyphenate and lrf.doc_info.lower().strip() == 'en' - self.lrf, self.logger, self.opts = lrf, logger, opts + self.logger, self.opts = logger, opts self.pages = [] - self.dpi = lrf.device_info.dpi/10. - self.ruby_tags = dict(**lrf.ruby_tags) - self.load_fonts() self.chapters = [] self.chapter_layout = None self.current_screen = None self.current_page = 0 - self.render_doc() + self.link_map = {} + self.chapter_map = {} + self.history = History() + + 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 load_fonts(self): + 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): font_map = {} - for font in self.lrf.font_map: - fdata = QByteArray(self.lrf.font_map[font].data) - id = QFontDatabase.addApplicationFontFromData(fdata) - font_map[font] = [str(i) for i in QFontDatabase.applicationFontFamilies(id)][0] - for font in FONT_MAP: - fdata = QByteArray(FONT_MAP[font].font_data) + 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] + + 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): - oddscreen, evenscreen = Screen(self.font_loader, chapter, True, self.logger, self.opts), \ - Screen(self.font_loader, chapter, False, self.logger, self.opts) + 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) + 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.chapters.append(Chapter(oddscreen, evenscreen, pages, object_to_page_map)) + self.chapter_map[chapter.id] = len(self.chapters)-1 - def render_doc(self): - for pt in self.lrf.page_trees: + def render(self, lrf): + self.dpi = lrf.device_info.dpi/10. + self.ruby_tags = dict(**lrf.ruby_tags) + self.load_fonts(lrf) + self.objects = lrf.objects + + num_chaps = 0 + for pt in lrf.page_trees: for chapter in pt: - self.render_chapter(chapter) + 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: + if num < 1 or num > self.num_of_pages or num == self.current_page: return - it = iter(self.chapters) - chapter = it.next() - num_before = 0 - while num > chapter.num_of_pages: - num_before += chapter.num_of_pages - chapter = it.next() odd = num%2 == 1 - page = chapter.page(num - num_before) + 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.removeItem(self.current_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.current_page = num + self.emit(SIGNAL('page_changed(PyQt_PyObject)'), self.current_page) + def next(self): - self.show_page(self.current_page + 1) + self.next_by(1) def previous(self): - self.show_page(self.current_page - 1) + self.previous_by(1) def next_by(self, num): self.show_page(self.current_page + num) @@ -557,4 +972,4 @@ class Document(QGraphicsScene): def show_page_at_percent(self, p): num = self.num_of_pages*(p/100.) self.show_page(num) - \ No newline at end of file + diff --git a/src/libprs500/gui2/lrf_renderer/main.py b/src/libprs500/gui2/lrf_renderer/main.py index 0ee6cd0569..44554c60aa 100644 --- a/src/libprs500/gui2/lrf_renderer/main.py +++ b/src/libprs500/gui2/lrf_renderer/main.py @@ -14,19 +14,40 @@ ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''''' -import sys, logging, os +import sys, logging, os, traceback -from PyQt4.Qt import QApplication, QCoreApplication -from PyQt4.QtCore import Qt, QObject +from PyQt4.QtGui import QApplication, QKeySequence, QPainter +from PyQt4.QtCore import Qt, QObject, SIGNAL, QCoreApplication, QThread from libprs500 import __appname__, __version__, __author__, setup_cli_handlers, islinux from libprs500.ebooks.lrf.parser import LRFDocument from libprs500.gui2 import ORG_NAME, APP_UID +from libprs500.gui2.dialogs.conversion_error import ConversionErrorDialog from libprs500.gui2.lrf_renderer.main_ui import Ui_MainWindow +from libprs500.gui2.main_window import MainWindow from libprs500.gui2.lrf_renderer.document import Document -class Main(QObject, Ui_MainWindow): +class RenderWorker(QThread): + + def __init__(self, parent, lrf_stream, logger, opts): + QThread.__init__(self, parent) + self.stream, self.logger, self.opts = lrf_stream, logger, opts + self.lrf = None + self.document = None + self.exception = None + + def run(self): + try: + self.lrf = LRFDocument(self.stream) + self.stream.close() + self.stream = None + except Exception, err: + self.exception = err + self.formatted_traceback = traceback.format_exc() + self.emit(SIGNAL('parsed()')) + +class Main(QObject, Ui_MainWindow, MainWindow): def __init__(self, window, stream, logger, opts): QObject.__init__(self) Ui_MainWindow.__init__(self) @@ -34,28 +55,95 @@ class Main(QObject, Ui_MainWindow): self.window = window self.logger = logger self.file_name = os.path.basename(stream.name) if hasattr(stream, 'name') else '' - self.stream = stream self.opts = opts + self.document = None + self.renderer = RenderWorker(self, stream, logger, opts) + QObject.connect(self.renderer, SIGNAL('parsed()'), self.parsed, Qt.QueuedConnection) + self.document = Document(self.logger, self.opts) + QObject.connect(self.document, SIGNAL('chapter_rendered(int)'), self.chapter_rendered) + QObject.connect(self.document, SIGNAL('page_changed(PyQt_PyObject)'), self.page_changed) + self.search.help_text = 'Search' + self.search.clear_to_help() + + self.action_next_page.setShortcuts(QKeySequence.MoveToNextPage) + self.action_previous_page.setShortcuts(QKeySequence.MoveToPreviousPage) + QObject.connect(self.action_next_page, SIGNAL('triggered(bool)'), self.next) + QObject.connect(self.action_previous_page, SIGNAL('triggered(bool)'), self.previous) + QObject.connect(self.action_back, SIGNAL('triggered(bool)'), self.back) + QObject.connect(self.action_forward, SIGNAL('triggered(bool)'), self.forward) + QObject.connect(self.spin_box, SIGNAL('valueChanged(int)'), self.go_to_page) + QObject.connect(self.slider, SIGNAL('valueChanged(int)'), self.go_to_page) + self.next_button.setDefaultAction(self.action_next_page) + self.previous_button.setDefaultAction(self.action_previous_page) + self.back_button.setDefaultAction(self.action_back) + self.forward_button.setDefaultAction(self.action_forward) + + self.graphics_view.setRenderHint(QPainter.Antialiasing, True) + self.graphics_view.setRenderHint(QPainter.TextAntialiasing, True) + self.graphics_view.setRenderHint(QPainter.SmoothPixmapTransform, True) + + def page_changed(self, num): + self.slider.setValue(num) + self.spin_box.setValue(num) + def render(self): - self.window.show() - self.statusbar.showMessage('Parsing LRF file '+self.file_name) - QCoreApplication.instance().processEvents() - self.lrf = LRFDocument(self.stream) - self.stream.close() - self.stream = None - self.graphics_view.resize(self.lrf.device_info.width+15, self.lrf.device_info.height) - self.window.setWindowTitle(self.lrf.metadata.title + ' - ' + __appname__) - self.statusbar.showMessage('Building graphical representation') - QCoreApplication.instance().processEvents() + self.stack.setCurrentIndex(1) + self.renderer.start() + + + def parsed(self, *args): + if self.renderer.lrf is not None: + self.graphics_view.setMinimumSize(self.renderer.lrf.device_info.width+5, + self.renderer.lrf.device_info.height) + self.window.setWindowTitle(self.renderer.lrf.metadata.title + ' - ' + __appname__) + self.document_title = self.renderer.lrf.metadata.title + self.document.render(self.renderer.lrf) + self.renderer.lrf = None + self.graphics_view.setScene(self.document) + self.graphics_view.show() + self.spin_box.setRange(1, self.document.num_of_pages) + self.slider.setRange(1, self.document.num_of_pages) + self.spin_box.setSuffix(' of %d'%(self.document.num_of_pages,)) + self.spin_box.updateGeometry() + self.stack.setCurrentIndex(0) + else: + exception = self.renderer.exception + print >>sys.stderr, 'Error rendering document' + print >>sys.stderr, exception + print >>sys.stderr, self.renderer.formatted_traceback + msg = u'

%s: '%(exception.__class__.__name__,) + unicode(str(exception), 'utf8', 'replace') + u'

' + msg += u'

Failed to render document

' + msg += u'

Detailed traceback:

'
+            msg += self.renderer.formatted_traceback + '
' + d = ConversionErrorDialog(self.window, 'Error while rendering file', msg) + d.exec_() + + def chapter_rendered(self, num): + if num > 0: + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(num) + self.progress_bar.setValue(0) + self.progress_label.setText('Rendering '+ self.document_title) + else: + self.progress_bar.setValue(self.progress_bar.value()+1) + QCoreApplication.processEvents() + + def next(self, triggered): + self.document.next() + def previous(self, triggered): + self.document.previous() - self.document = Document(self.lrf, self.logger, self.opts) - self.graphics_view.setScene(self.document) - self.graphics_view.show() - self.statusbar.clearMessage() - QCoreApplication.instance().processEvents() - self.document.next() + def go_to_page(self, num): + self.document.show_page(num) + + def forward(self, triggered): + self.document.forward() + + def back(self, triggered): + self.document.back() + def file_renderer(stream, logger, opts): from PyQt4.Qt import QMainWindow @@ -93,9 +181,13 @@ def main(args=sys.argv, logger=None): QCoreApplication.setOrganizationName(ORG_NAME) QCoreApplication.setApplicationName(APP_UID) main = file_renderer(open(args[1], 'rb'), logger, opts) + sys.excepthook = main.unhandled_exception + main.window.show() main.render() return app.exec_() return 0 if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file + sys.exit(main()) + + \ No newline at end of file diff --git a/src/libprs500/gui2/lrf_renderer/main.ui b/src/libprs500/gui2/lrf_renderer/main.ui index eef1b7e4e5..99fde22556 100644 --- a/src/libprs500/gui2/lrf_renderer/main.ui +++ b/src/libprs500/gui2/lrf_renderer/main.ui @@ -5,46 +5,254 @@ 0 0 - 858 - 854 + 614 + 832 - + 0 0 - MainWindow + LRF Viewer - - - - - 0 - 10 - 281 - 321 - + + :/library + + + + + 0 - - true + + 0 - + + 0 + + + 0 + + + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + ... + + + + + + + ... + + + + + + + Qt::Horizontal + + + + + + + ... + + + + + + + + 0 + 0 + + + + + + + + ... + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + 0 + + + -1 + + + + + + + + 11 + 75 + true + + + + Parsing LRF file + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + - - - - 0 - 0 - 858 - 29 - + + + :/images/next.svg - - + + Next Page + + + + + :/images/previous.svg + + + Previous Page + + + + + :/images/back.svg + + + Back + + + + + :/images/forward.svg + + + Forward + + - + + + SearchBox + QLineEdit +
library.h
+
+
+ + + diff --git a/src/libprs500/gui2/main.py b/src/libprs500/gui2/main.py index ce556bd604..20c92e6fe7 100644 --- a/src/libprs500/gui2/main.py +++ b/src/libprs500/gui2/main.py @@ -12,7 +12,7 @@ ## 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.Warning -import os, sys, traceback, StringIO, textwrap +import os, sys, textwrap from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ QSettings, QVariant, QSize, QThread @@ -30,6 +30,7 @@ from libprs500.devices.interface import Device from libprs500.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \ initialize_file_icon_provider, BOOK_EXTENSIONS, \ pixmap_to_data, choose_dir, ORG_NAME +from libprs500.gui2.main_window import MainWindow from libprs500.gui2.main_ui import Ui_MainWindow from libprs500.gui2.device import DeviceDetector, DeviceManager from libprs500.gui2.status import StatusBar @@ -41,7 +42,7 @@ from libprs500.gui2.dialogs.conversion_error import ConversionErrorDialog from libprs500.gui2.dialogs.lrf_single import LRFSingleDialog from libprs500.gui2.dialogs.password import PasswordDialog -class Main(QObject, Ui_MainWindow): +class Main(QObject, Ui_MainWindow, MainWindow): def set_default_thumbnail(self, height): r = QSvgRenderer(':/images/book.svg') @@ -665,15 +666,7 @@ class Main(QObject, Ui_MainWindow): e.accept() - def unhandled_exception(self, type, value, tb): - sio = StringIO.StringIO() - traceback.print_exception(type, value, tb, file=sio) - fe = sio.getvalue() - print >>sys.stderr, fe - msg = '

' + unicode(str(value), 'utf8', 'replace') + '

' - msg += '

Detailed traceback:

'+fe+'
' - d = ConversionErrorDialog(self.window, 'ERROR: Unhandled exception', msg) - d.exec_() + def main(args=sys.argv): from PyQt4.Qt import QApplication, QMainWindow diff --git a/src/libprs500/gui2/main_window.py b/src/libprs500/gui2/main_window.py new file mode 100644 index 0000000000..e5d7374113 --- /dev/null +++ b/src/libprs500/gui2/main_window.py @@ -0,0 +1,30 @@ +## 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 StringIO, traceback, sys + +from libprs500.gui2.dialogs.conversion_error import ConversionErrorDialog + +class MainWindow(object): + + def unhandled_exception(self, type, value, tb): + sio = StringIO.StringIO() + traceback.print_exception(type, value, tb, file=sio) + fe = sio.getvalue() + print >>sys.stderr, fe + msg = '

' + unicode(str(value), 'utf8', 'replace') + '

' + msg += '

Detailed traceback:

'+fe+'
' + d = ConversionErrorDialog(self.window, 'ERROR: Unhandled exception', msg) + d.exec_() \ No newline at end of file