PyXR

c:\projects\bitpim\src \ bphtml.py



0001 ### BITPIM
0002 ###
0003 ### Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com>
0004 ###
0005 ### This program is free software; you can redistribute it and/or modify
0006 ### it under the terms of the BitPim license as detailed in the LICENSE file.
0007 ###
0008 ### $Id: bphtml.py 4380 2007-08-29 00:17:07Z djpham $
0009 
0010 # Standard modules
0011 from __future__ import with_statement
0012 import copy
0013 import webbrowser
0014 import re
0015 import htmlentitydefs
0016 import HTMLParser
0017 import StringIO
0018 
0019 # wx modules
0020 import wx
0021 import wx.html
0022 
0023 # my modules
0024 import guihelper
0025 import fixedwxpTag
0026 import common
0027 
0028 ###
0029 ###  Enhanced HTML Widget
0030 ###
0031 
0032 class HTMLWindow(wx.html.HtmlWindow):
0033     """BitPim customised HTML Window
0034 
0035     Some extras on this:
0036     
0037        - You can press Ctrl-Alt-S to get a source view
0038        - Clicking on a link opens a window in your browser
0039        - Shift-clicking on a link copies it to the clipboard
0040     """
0041     def __init__(self, parent, id, relsize=0.7):
0042         wx.html.HtmlWindow.__init__(self, parent, id)
0043         wx.EVT_KEY_UP(self, self.OnKeyUp)
0044         self.thetext=""
0045         self._normal_font_size=max(wx.NORMAL_FONT.GetPointSize(), 10)
0046         self.SetFontScale(relsize)
0047 
0048     def SetFontScale(self, scale):
0049         self.SetStandardFonts(int(scale*self._normal_font_size), '', '')
0050         # the html widget clears itself if you set the scale
0051         if len(self.thetext):
0052             wx.html.HtmlWindow.SetPage(self,self.thetext)
0053 
0054 ##    def OnCellMouseHover(self, cell, x, y):
0055 ##        print cell
0056 ##        print dir(cell)
0057 ##        print cell.GetId()
0058 
0059     def OnLinkClicked(self, event):
0060         # see ClickableHtmlWindow in wxPython source for inspiration
0061         # :::TODO::: redirect bitpim images and audio to correct
0062         # player
0063         if event.GetEvent().ShiftDown():
0064             wx.TheClipboard.Open()
0065             wx.TheClipboard.SetData(event.GetHref())
0066             wx.TheClipboard.Close()
0067         else:
0068             webbrowser.open(event.GetHref())
0069 
0070     def SetPage(self, text):
0071         self.thetext=text
0072         wx.html.HtmlWindow.SetPage(self,text)
0073 
0074     def OnKeyUp(self, evt):
0075         keycode=evt.GetKeyCode()        
0076         if keycode==ord('S') and evt.ControlDown() and evt.AltDown():
0077             vs=ViewSourceFrame(None, self.thetext)
0078             vs.Show(True)
0079             evt.Skip()
0080 
0081 ###
0082 ###  View Source Window
0083 ###            
0084 
0085 class ViewSourceFrame(wx.Frame):
0086     def __init__(self, parent, text, id=-1):
0087         wx.Frame.__init__(self, parent, id, "HTML Source")
0088         stc=wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE)
0089         stc.AppendText(text)
0090 
0091 ###
0092 ### HTML Parsing for our pseudo styles system
0093 ###
0094 
0095 class TreeParser(HTMLParser.HTMLParser):
0096     """Turns the HTML data into a tree structure
0097 
0098     Note that the HTML needs to be well formed (ie closing tags must be present)"""
0099 
0100     class TreeNode:
0101 
0102         def __init__(self):
0103             self.tag=""
0104             self.attrs=[]
0105             self.children=[]
0106             self.data=""
0107             self.styles=[]
0108     
0109     def __init__(self, data):
0110         HTMLParser.HTMLParser.__init__(self)
0111         self.rootnode=self.TreeNode()
0112         self.nodestack=[self.rootnode]
0113         self.feed(data)
0114         self.close()
0115         assert len(self.rootnode.children)==1
0116         self.rootnode=self.rootnode.children[0]
0117         assert self.rootnode.tag=="html"
0118         # self.mergedata()
0119 
0120     def handle_starttag(self, tag, attrs):
0121         # print "start",tag,attrs
0122         node=self.TreeNode()
0123         node.tag=tag
0124         node.attrs=attrs
0125         self.nodestack[-1].children.append(node)
0126         self.nodestack.append(node)
0127 
0128     def handle_endtag(self, tag):
0129         # print "end",tag
0130         if tag==self.nodestack[-1].tag:
0131             self.nodestack=self.nodestack[:-1]
0132         else:
0133             print tag,"doesn't match tos",self.nodestack[-1].tag
0134             self.printtree()
0135             assert False, "HTML is not well formed"
0136 
0137 
0138     def handle_entityref(self, name):
0139         data=htmlentitydefs.entitydefs.get(name, None)
0140         if data is None:
0141             self.handle_data('&%s'%name)
0142         elif data=="\xa0": # hard space
0143             return
0144         else:
0145             self.handle_data("&%s;" % (name,))
0146             
0147     def handle_data(self, data):
0148         if len(data.strip())==0:
0149             return
0150         # print "data",data
0151         node=self.TreeNode()
0152         node.data=data
0153         self.nodestack[-1].children.append(node)
0154 
0155     def printtree(self, node=None, indent=0):
0156         if node is None:
0157             node=self.rootnode
0158         ins="  "*indent
0159         if len(node.data):
0160             print ins+`node.data`
0161             assert len(node.children)==0
0162             assert len(node.attrs)==0
0163         else:
0164             print ins+"<"+node.tag+"> "+`node.attrs`
0165             for c in node.children:
0166                self.printtree(c, indent+1)
0167 
0168     def flatten(self):
0169         io=StringIO.StringIO()
0170         self._flatten(self.rootnode, io)
0171         return io.getvalue()
0172 
0173     _nltags=("p", "head", "title", "h1", "h2", "h3", "h4", "h5", "table", "tr")
0174     def _flatten(self, node, io):
0175         if len(node.data):
0176             io.write(node.data)
0177             return
0178 
0179         if node.tag in self._nltags:
0180             io.write("\n")
0181 
0182         io.write("<%s" % (node.tag,))
0183         for a,v in node.styles:
0184             io.write(' %s="%s"' % (a,v))
0185         for a,v in node.attrs:
0186             io.write(' %s="%s"' % (a,v))
0187         io.write(">")
0188 
0189         for child in node.children:
0190             self._flatten(child,io)
0191 
0192         io.write("</%s>" % (node.tag,))
0193 
0194         if node.tag in self._nltags:
0195             io.write("\n")
0196 
0197                  
0198 
0199 ###
0200 ###  Turn HTML with style classes in it into expanded HTML without classes
0201 ###  This is needed as wxHTML doesn't support style classes
0202 ###
0203 
0204 def applyhtmlstyles(html, styles):
0205     tp=TreeParser(html)
0206     _applystyles(tp.rootnode, styles)
0207     return tp.flatten()
0208 
0209 def _hasclass(node):
0210     for a,_ in node.attrs:
0211         if a=="class":
0212             return True
0213     return False
0214 
0215 def _applystyles(node, styles):
0216     if len(node.data):
0217         return
0218 
0219     # wxp tags are left alone
0220     if node.tag=="wxp":
0221         return
0222 
0223     if _hasclass(node):
0224         newattrs=[]
0225         for a,v in node.attrs:
0226             if a!="class":
0227                 newattrs.append( (a,v) )
0228                 continue
0229             c=styles.get(v,None)
0230             if c is None:
0231                 continue
0232             _applystyle(node, c)
0233         node.attrs=newattrs
0234 
0235     for c in node.children:
0236         _applystyles(c, styles)
0237 
0238 def _applystyle(node, style):
0239     if len(style)==0: return
0240     if len(node.data): return
0241     s=style.get('', None)
0242     if s is not None:
0243         assert len(s)&1==0 # must even number of items
0244         for i in range(len(s)/2):
0245             node.styles.append( (s[i*2], s[i*2+1]) )
0246         style=style.copy()
0247         del style['']
0248     # do we have any add styles
0249     if len([k for k in style if k[0]=='+']):
0250         newstyle={}
0251         for k in style:
0252             if k[0]!='+':
0253                 newstyle[k]=style[k]
0254                 continue
0255             # make a child node with this style in it
0256             kid=TreeParser.TreeNode()
0257             kid.tag=k[1:]
0258             kid.children=node.children
0259             node.children=[kid]
0260             # copy style
0261             s=style[k]
0262             assert len(s)&1==0 # must even number of items
0263             for i in range(len(s)/2):
0264                 kid.styles.append( (s[i*2], s[i*2+1]) )
0265         style=newstyle
0266     if len(style)==0: return
0267     # ok, apply style to us and any children
0268     if node.tag in style:
0269         s=style[node.tag]
0270         assert len(s)&1==0 # must even number of items
0271         for i in range(len(s)/2):
0272             node.styles.append( (s[i*2], s[i*2+1]) )
0273     for i in node.children:
0274         _applystyle(i, style)
0275 
0276 class PrintData(wx.PrintData):
0277     """ Similar to wx.PrintData except this one includes copy ctor and
0278     copy 'operator'
0279     """
0280     # the names of Get/Set attributes to copy from source to dest object
0281     _attr_names=("Collate", "Colour", "Duplex", "NoCopies",
0282                  "Orientation", "PaperId", "PrinterName",
0283                  "PaperSize")
0284     def __init__(self, rhs=None):
0285         super(PrintData, self).__init__()
0286         if rhs is not None:
0287             self._copy(rhs, self)
0288 
0289     def _copy(self, src, dest):
0290         for attr in self._attr_names:
0291             getattr(dest, 'Set'+attr)(getattr(src, 'Get'+attr)())
0292 
0293     def copy(self):
0294         return PrintData(self)
0295 
0296 class HtmlEasyPrinting:
0297     """Similar to wxHtmlEasyPrinting, except this is actually useful.
0298 
0299     The following additions are present:
0300 
0301       - The various settings are saved in a supplied config object
0302       - You can set the scale for the fonts, otherwise the default
0303         is way too large (you would get about 80 chars across)
0304     """
0305     import guiwidgets
0306 
0307     def __init__(self, parent=None, config=None, configstr=None):
0308         self.parent=parent
0309         self.config=config
0310         self.configstr=configstr
0311         self.printData=PrintData()
0312         self._configtoprintdata(self.printData)
0313         # setup margins
0314         self._configtopagesetupdata()
0315 
0316     def SetParentFrame(self, parent):
0317         self.parent=parent
0318 
0319     def PrinterSetup(self):
0320         with guihelper.WXDialogWrapper(wx.PrintDialog(self.parent)) as printerDialog:
0321             if self.printData.Ok():
0322                 printerDialog.GetPrintDialogData().SetPrintData(self.printData.copy())
0323             printerDialog.GetPrintDialogData().SetSetupDialog(True)
0324             if printerDialog.ShowModal()==wx.ID_OK:
0325                 self.printData = PrintData(printerDialog.GetPrintDialogData().GetPrintData())
0326                 self._printdatatoconfig(self.printData)
0327 
0328     def PageSetup(self):
0329         psdd=wx.PageSetupDialogData()
0330         if self.printData.Ok():
0331             psdd.SetPrintData(self.printData.copy())
0332         self._configtopagesetupdata(psdd)
0333         with guihelper.WXDialogWrapper(wx.PageSetupDialog(self.parent, psdd)) as pageDialog:
0334             if pageDialog.ShowModal()==wx.ID_OK and \
0335                pageDialog.GetPageSetupData().Ok() and \
0336                pageDialog.GetPageSetupData().GetPrintData().Ok():
0337                 self.printData=PrintData(pageDialog.GetPageSetupData().GetPrintData())
0338                 self._printdatatoconfig(self.printData)
0339                 self._pagesetupdatatoconfig(pageDialog.GetPageSetupData())
0340 
0341     def PreviewText(self, htmltext, basepath="", scale=1.0):
0342         printout1=self._getprintout(htmltext, basepath, scale)
0343         printout2=self._getprintout(htmltext, basepath, scale)
0344         if self.printData.Ok():
0345             pd=self.printData.copy()
0346         else:
0347             pd=PrintData()
0348         if not pd.Orientation:
0349             pd.SetOrientation(wx.PORTRAIT)
0350         preview=wx.PrintPreview(printout1, printout2, pd)
0351         if not preview.Ok():
0352             print "preview problem"
0353             assert False, "preview problem"
0354             return
0355         self.frame=wx.PreviewFrame(preview, self.parent, "Print Preview")
0356         self.guiwidgets.set_size("PrintPreview", self.frame, screenpct=90, aspect=0.58)
0357         wx.EVT_CLOSE(self.frame, self.OnPreviewClose)
0358         self.frame.Initialize()
0359         # self.frame.SetPosition(self.parent.GetPosition())
0360         # self.frame.SetSize(self.parent.GetSize())
0361         self.frame.Show(True)
0362 
0363     def PrintText(self, htmltext, basepath="", scale=1.0):
0364         pdd=wx.PrintDialogData()
0365         if self.printData.Ok():
0366             pdd.SetPrintData(self.printData.copy())
0367         if not pdd.GetPrintData().Orientation:
0368             pdd.GetPrintData().SetOrientation(wx.PORTRAIT)
0369         printer=wx.Printer(pdd)
0370         with guihelper.WXDialogWrapper(self._getprintout(htmltext, basepath, scale)) \
0371              as printout:
0372             printer.Print(self.parent, printout)
0373 
0374     def _getprintout(self, htmltext, basepath, scale):
0375         printout=wx.html.HtmlPrintout()
0376         basesizes=[7,8,10,12,16,22,30]
0377         printout.SetFonts("", "", [int(sz*scale) for sz in basesizes])
0378         printout.SetMargins(*self.margins)
0379         printout.SetHtmlText(htmltext, basepath)
0380         return printout
0381 
0382     def _printdatatoconfig(self, pd):
0383         if self.config is None or self.configstr is None or not pd.Ok():
0384             print '_printdatatoconfig: bad printData'
0385             return
0386         c=self.config
0387         cstr=self.configstr
0388 
0389         for key,func in [ ("collate", pd.GetCollate),
0390                           ("colour", pd.GetColour),
0391                           ("duplex", pd.GetDuplex),
0392                           ("nocopies", pd.GetNoCopies),
0393                           ("orientation", pd.GetOrientation),
0394                           ("paperid", pd.GetPaperId),
0395                           ]:
0396             c.WriteInt(cstr+"/"+key, func())
0397         c.Write(cstr+"/printer", pd.GetPrinterName())
0398         c.Flush()
0399         
0400     def _configtoprintdata(self, pd):
0401         if self.config is None or self.configstr is None:
0402             return
0403         c=self.config
0404         cstr=self.configstr
0405         if not pd.Ok():
0406             print '_configtoprintdata: bad printData'
0407             return
0408         for key,func in [ ("collate", pd.SetCollate),
0409                           ("colour", pd.SetColour),
0410                           ("duplex", pd.SetDuplex),
0411                           ("nocopies", pd.SetNoCopies),
0412                           ("orientation", pd.SetOrientation),
0413                           ("paperid", pd.SetPaperId),
0414                           ]:
0415             if c.HasEntry(cstr+"/"+key):
0416                 func(c.ReadInt(cstr+"/"+key))
0417         # special case - set paper to letter if not explicitly set (wx defaults to A4)
0418         if not c.HasEntry(cstr+"/paperid"):
0419             pd.SetPaperId(wx.PAPER_LETTER)
0420         # printer name
0421         pd.SetPrinterName(c.Read(cstr+"/printer", ""))
0422 
0423     def _configtopagesetupdata(self, psdd=None):
0424         v=self.config.Read(self.configstr+"/margins", "")
0425         if len(v):
0426             l=[int(x) for x in v.split(',')]
0427         else:
0428             l=[15,15,15,15]
0429         if psdd is not None:
0430             psdd.SetMarginTopLeft( (l[2], l[0]) )
0431             psdd.SetMarginBottomRight( (l[3], l[1]) )
0432         self.margins=l
0433 
0434     def _pagesetupdatatoconfig(self, psdd):
0435         tl=psdd.GetMarginTopLeft()
0436         br=psdd.GetMarginBottomRight()
0437         v="%d,%d,%d,%d" % (tl[1], br[1], tl[0], br[0])
0438         self.config.Write(self.configstr+"/margins", v)
0439         self.margins=[tl[1],br[1],tl[0],br[0]]
0440 
0441     def OnPreviewClose(self, event):
0442         self.guiwidgets.save_size("PrintPreview", self.frame.GetRect())
0443         event.Skip()
0444 
0445 # scaling factor for the various font sizes
0446 _scales=[0.7, 0.9, 1, 1.1, 1.2, 1.4, 1.6]
0447 
0448 
0449 # this is just used to get a namespace
0450 class Renderer:
0451 
0452     _lastsize=None
0453     _lastfont=None
0454     _mdc=None
0455     _bmp=None
0456     _hdc=None
0457 
0458     @classmethod
0459     def getbestsize(_, html, basepath, font, size):
0460         if Renderer._bmp is None:
0461             Renderer._bmp=wx.EmptyBitmap(1,1)
0462             Renderer._mdc=wx.MemoryDC()
0463             Renderer._mdc.SelectObject(Renderer._bmp)
0464             Renderer._hdc=wx.html.HtmlDCRenderer()
0465             Renderer._hdc.SetDC(Renderer._mdc, 1)
0466             Renderer._hdc.SetSize(99999,99999)
0467         Renderer._mdc.ResetBoundingBox()
0468         if Renderer._lastsize!=size or Renderer._lastfont!=font:
0469             Renderer._hdc.SetFonts(font, "", [int(x*size) for x in _scales])
0470         Renderer._hdc.SetHtmlText(html, basepath)
0471         Renderer._hdc.Render(0, 0, (), 0, False)
0472         return (Renderer._mdc.MaxX(), Renderer._mdc.MaxY())        
0473 
0474 def getbestsize(dc, html, basepath="", font="", size=10):
0475     return Renderer.getbestsize(html, basepath, font, size)
0476 
0477 def drawhtml(dc, rect, html, basepath="", font="", size=10):
0478     """Draw html into supplied dc and rect"""
0479     if html is None or html=="":
0480         return
0481     origscale=dc.GetUserScale()
0482     hdc=wx.html.HtmlDCRenderer()
0483     hdc.SetFonts(font, "", [int(x*size) for x in _scales])
0484     hdc.SetDC(dc, 1)
0485     hdc.SetSize(rect.width, rect.height)
0486     hdc.SetHtmlText(html, basepath)
0487     hdc.Render(rect.x, rect.y, (), 0, False)
0488     dc.SetUserScale(*origscale)
0489 
0490 
0491 # done down here to prevent circular imports
0492 ##import guiwidgets
0493 
0494         
0495 if __name__=='__main__':
0496     src="""
0497 <HTML>
0498 <head><title>A title</title></head>
0499 
0500 <body>
0501 <h1 cLaSs="gaudy">Heading 1</h1>
0502 
0503 <p>This is my sentence <span class=hilite>with some hilite</span></p>
0504 
0505 <p><table><tr><th>one</th><th>two</th></tr>
0506 <tr><td class="orange">orange</td><td>Normal</td></tr>
0507 <tr class="orange"><td>whole row is</td><td>orange</td></tr>
0508 </table></p>
0509 </body>
0510 </html>
0511 """
0512     styles={
0513         'gaudy': 
0514         {
0515         '+font': ('color', '#123456'),
0516         '': ('bgcolor', 'grey'),
0517         },
0518         
0519         'orange':
0520         {
0521         '+font': ('color', '#001122'),
0522         'tr': ('bgcolor', '#cc1122'),
0523         'td': ('bgcolor', '#cc1122'),
0524         },
0525     
0526         'hilite':
0527         {
0528         '+b': (),
0529         '+font': ('color', '#564bef'),
0530         }
0531                    
0532         }
0533 
0534     tp=TreeParser(src)
0535     _applystyles(tp.rootnode, styles)
0536     tp.printtree()
0537     print tp.flatten()
0538 
0539     app=wx.PySimpleApp()
0540 
0541     f=wx.Frame(None, -1, "HTML Test")
0542     h=HTMLWindow(f, -1)
0543     f.Show(True)
0544     h.SetPage(tp.flatten())
0545     app.MainLoop()
0546     sys.exit(0)
0547 
0548 # a test of the easy printing
0549 if __name__=='__main__':
0550     import sys
0551 
0552     def OnSlider(evt):
0553         global scale
0554         scale=szlist[evt.GetPosition()]
0555 
0556     f=common.opentextfile(sys.argv[1])
0557     html=f.read()
0558     f.close()
0559 
0560     app=wx.PySimpleApp()
0561 
0562     f=wx.Frame(None, -1, "Print Test")
0563     butsetup=wx.Button(f, wx.NewId(), "Setup")
0564     butpreview=wx.Button(f, wx.NewId(), "Preview")
0565     butprint=wx.Button(f, wx.NewId(), "Print")
0566 
0567     slider=wx.Slider(f, wx.NewId(), 5, 0, 10, style=wx.SL_HORIZONTAL)
0568 
0569     szlist=[0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4]
0570 
0571     scale=1.0
0572 
0573     bs=wx.BoxSizer(wx.HORIZONTAL)
0574     bs.Add(slider, 1, wx.EXPAND|wx.ALL, 5)
0575     for i in (butsetup,butpreview, butprint):
0576         bs.Add(i, 0, wx.EXPAND|wx.ALL, 5)
0577     f.SetSizer(bs)
0578     bs.Fit(f)
0579 
0580     hep=HtmlEasyPrinting(f, None, None)
0581 
0582     wx.EVT_BUTTON(f, butsetup.GetId(), lambda _: hep.PrinterSetup())
0583     wx.EVT_BUTTON(f, butpreview.GetId(), lambda _: hep.PreviewText(html, scale=scale))
0584     wx.EVT_BUTTON(f, butprint.GetId(), lambda _: hep.PrintText(html, scale=scale))
0585     wx.EVT_COMMAND_SCROLL(f, slider.GetId(), OnSlider)
0586 
0587     f.Show(True)
0588     app.MainLoop()
0589     
0590 

Generated by PyXR 0.9.4