Comments on Printing

In the wxPython users list, there are regularly comments and questions about printing in wxPython. In the present page, I would like to add some comments. These are coming from my personal experience as a wxPython user.

Printing or preparing the printing process under wxPython is not an easy task. There are a lot of points to consider, font size, dc size, preview, paper format, scaling... . Each point, itself, is working fine, but the interaction between them can lead to "unexpected" results.

Font size

The simplest way is certainly to set a font size in points and to live with it. Unfortunately, all printers do not "understand" the font size in the same manner. This method does not work for previewing, because the preview window gives the user a chance to set the document scale but font sizes are not adjusted properly.

Note that the ideal solution is to use the DC SetUserScale method to automatically scale all document elements according the current resolution. The solution described here is rather different, as it changes only the font size to compensate for higher resolution. This is possible when the document is text-only, and necessary when implementation bugs make using SetUserScale problematic.

To scale fonts for printing, make sure you set a font size proportional to the size of the dc associated to the device. This dc represents the printable area of a printer page or the preview canvas in a preview window. The size of this dc can be obtained via the wxPrintout object. The demo uses this approach and it is globally working fine.

I propose here a small refinement to this approach. I do not claim, this is the best solution, but I get satisfying and practical results. When preparing the printing job, I define my own font size unit, fsu, which is proportional to the size of the dc.

   1     fsu = dc.GetSizeTuple()[0] / 750.0

This is a float type. The magic 750.0 number is a trial and error number, it corresponds more or less to the width of the dc for a previewing of 100%. Later, I am using this fsu to define the font size I want to use in my plot job.

   1     fs = int(fsu * 10 + 0.5)
   2     if fs < 1: fs = 1
   3     dc.SetFont(wxFont(fs, wxSWISS, wxNORMAL, wxNORMAL))


If you don't want to play with some magic numbers, you may try to use more science-based solution ;) Let's say, we will always specify font size in points (1/72 of inch) like in many text processors. The clue is to recalculate font size for current device context resolution (PPI - pixel per inch). To do this we need physical page size (wxPrintout.GetPageSizeMM give as size in milimeters), number of pixels in current device context and knowledge, that one inch is equivalent to 25.4 milimeters.

First, we calculate current "physical" (sheet of paper or preview canvas) page resolution in PPI:

   1     # device context page width in pixels
   2     if wxPrintout.IsPreview():
   3         w_px = wxPrintout.GetDC().GetSizeTuple()[0]
   4     else:
   5         w_px = wxPrintout.GetPageSizePixels()[0]
   6 
   7     # number of milimeters per inch
   8     mpi = 25.4
   9 
  10     # physical page width in mm
  11     w_mm = wxPrintout.GetPageSizeMM()[0]
  12 
  13     page_ppi = (w_px * mpi) / w_mm

Some comments on hand-calculated page PPI. Preview DC PPI is always constant, equal to screen PPI, so it's almost always inadequate to current size of preview DC. Maybe you don't have to calculate it for printer DC, but I think it's better to use universal algorithm for both - it's not much time consuming and will always work fine.

Now, we have to calculate font size scaling factor:

   1     # dc_ppi - device context page pixel per inch resolution
   2     if wxPrintout.IsPreview():
   3         dc_ppi = self.GetPPIScreen()[0]
   4     else:
   5         dc_ppi = self.GetPPIPrinter()[0]
   6 
   7     scale_factor = page_ppi / dc_ppi

Finally, we can calculate font size for current DC:

   1     # font size in points (like in word processor - you know ;)
   2     des_font_size = 12
   3 
   4     fs = des_font_size * scale_factor

Looks pretty awful? I hope it doesn't :) Now, let's try to glue it together and write some real-life code (as a part of wxPrintout derivative class method):

   1 class myPrintout(wxPrintout):
   2 
   3     # (...)
   4 
   5     def OnPrintPage(self, page):
   6 
   7     # (...)
   8 
   9         if self.IsPreview():
  10             dc_ppi = self.GetPPIScreen()[0]
  11             w_px = self.GetDC().GetSizeTuple()[0]
  12         else:
  13             dc_ppi = self.GetPPIPrinter()[0]
  14             w_px = self.GetPageSizePixels()[0]
  15 
  16         w_mm = self.GetPageSizeMM()[0]
  17 
  18         my_fs = 30 # desirable font size in points (1/72 of inch)
  19         mpi = 25.4
  20         fs = round((my_fs * w_px * mpi) / (w_mm * dc_ppi))
  21 
  22         # put some font initialization here with calculated font size (fs)

I hope this will help somebody :)

greetz from sototh


Using Python 2.3, do not forget to use integer font size, in order to prevent the "Deprecation warning: get float, integer expected..." or something like this. This simple approach is working fine for printing and for previewing at all zoom levels. Note that I am using this to generate one-page plots, not pages of long text.

You should be aware that this approach has some drawbacks.



Working with "print preview" is a good way to illustrate this phenomenon. Prepare a text of, let's say, 60 lines and define a font size proportional to the dc size. Now play with different zoom values, you will notice the print preview page/canvas is not always displaying the same amount of lines.

wxHTMLEasyPrinting is one another illustration, where previewing and printing do no match.

A way to get around with this issue would be to be able to use non integer font sizes. This is not possible on wxPython.

We have seen the printable area varies from one printer to the other. One another and probably more elegant approach is to set the font size proportional to the physical page size. This is a way to set a font size a little bit more printer independent. Of course, this depends on the page format, us-letter, A4,...

Finally, you should be aware that by defining a font size, you set both the x- and y-dimension at the same time. If you scale your font with the dc size, you have to choose between the height or the width of the dc.

The size of the dc and printout

When printing, it is easy to get the size of the dc, but it is impossible to know where this dc area is sitting on the physical page. In others words, it is impossible to get the size of the non printable margins. This information is not available using wxPython. However, these sizes can be obtained by other means. The simpliest way to get it is probably to use Word or OpenOffice.org. If you have done this experience, you may have notice that the size of the printable area returned by wxPython, wxPrintout::GetPageSizeMM() is not exact. It is a rounded / truncated(?) value.

A way to make wxPython users more happy could be to create some kind of data base on a wiki page. After all, there are not so many printers on the markets. I am thinking about a python module holding a dictionary like this:

   1 NonPrintableMargins = {'None': (,), \
   2                        'HP Deskjet 500': (3.2, 6.3, 3.6, 12.8), \
   3                        'printer A': (....) }

If you are artful enough, it is still possible to find a acceptable solution, as shown in the MyPrintPreview7 application.

MyPrintPreview7.py

An application that illustrates the above discussion.

That's all. My English is certainly not the best one, but the spirit of the story should be intelligible.

Jean-Michel Fauth, Switzerland 22 October 2002

   1 #-------------------------------------------------------------------
   2 #-*- coding: iso-8859-1 -*-
   3 #-------------------------------------------------------------------
   4 # MyPrintPreview7.py
   5 # wxPython 2.4.2.4, Python 2.3.0, win 98
   6 # Jean-Michel Fauth, Switzerland
   7 # 22 October 2003
   8 #-------------------------------------------------------------------
   9 
  10 from wxPython.wx import *
  11 
  12 #-------------------------------------------------------------------
  13 
  14 #constants
  15 DlgHeightCorr = 25   #caption bar + borders for default style, pixels
  16 DlgWidthCorr = 6     #borders for default style, pixels
  17 
  18 PageA4PortraitWidth_mm =  210.0
  19 PageA4PortraitHeight_mm = 297.0
  20 PageA4LandscapeWidth_mm =  297.0
  21 PageA4LandscapeHeight_mm = 210.0
  22 
  23 #for my hp deskjet 500, numbers from wxPython/printout
  24 PrintableAreaPortraitWidth_pix = 2400.0
  25 PrintableAreaPortraitHeight_pix = 3282.0
  26 PrintableAreaPortraitWidth_mm = 203.0
  27 PrintableAreaPortraitHeight_mm = 278.0
  28 
  29 PrintableAreaLandscapeWidth_pix = 3282.0
  30 PrintableAreaLandscapeHeight_pix = 2400.0
  31 PrintableAreaLandscapeWidth_mm = 278.0
  32 PrintableAreaLandscapeHeight_mm = 203.0
  33 
  34 #for my hp deskjet 500, numbers from other tools eg word
  35 NonPrintableMarginPortraitLeft_mm = 3.2
  36 NonPrintableMarginPortraitTop_mm = 6.3
  37 NonPrintableMarginPortraitRight_mm = 3.6
  38 NonPrintableMarginPortraitBottom_mm = 12.8
  39 
  40 NonPrintableMarginLandscapeLeft_mm = 6.3
  41 NonPrintableMarginLandscapeTop_mm = 3.6
  42 NonPrintableMarginLandscapeRight_mm = 12.8
  43 NonPrintableMarginLandscapeBottom_mm = 3.2
  44 
  45 #-------------------------------------------------------------------
  46 
  47 #convert mm into pixels (float)
  48 def mm2pix(mm):
  49     return mm / PrintableAreaPortraitWidth_mm * PrintableAreaPortraitWidth_pix
  50 
  51 #-------------------------------------------------------------------
  52 
  53 #a dc independent print job, used for printing and previewing
  54 def PrepareReducedPageA4Printout(dc, q, orientation):
  55     dcwi, dche = dc.GetSizeTuple()
  56     dc.BeginDrawing()
  57     
  58     #rectangle on dc limits
  59     dc.SetBrush(wxBrush('#ffffff', wxTRANSPARENT ))
  60     dc.SetPen(wxPen(wxRED))
  61     dc.DrawRectangle(0, 0, dcwi, dche) 
  62     
  63     #a square centered on the page
  64     #offsets
  65     if orientation == 'portrait':
  66         #margin right > margin left !
  67         dhalf_mm = (NonPrintableMarginPortraitRight_mm - NonPrintableMarginPortraitLeft_mm) / 2.0
  68         dhalf_pix = mm2pix(dhalf_mm)
  69         xoff = int(dhalf_pix / q)
  70         #margin bottom > margin top !
  71         dhalf_mm = (NonPrintableMarginPortraitBottom_mm - NonPrintableMarginPortraitTop_mm) / 2.0
  72         dhalf_pix = mm2pix(dhalf_mm)
  73         yoff = int(dhalf_pix / q)
  74         print 'q:', q
  75         print 'x,y off:', xoff, yoff    
  76     else: #landscape
  77         #margin right > margin left !
  78         dhalf_mm = (NonPrintableMarginLandscapeRight_mm - NonPrintableMarginLandscapeLeft_mm) / 2.0
  79         dhalf_pix = mm2pix(dhalf_mm)
  80         xoff = int(dhalf_pix / q)
  81         #margin top > margin bottom !
  82         dhalf_mm = (NonPrintableMarginLandscapeTop_mm - NonPrintableMarginLandscapeBottom_mm) / 2.0
  83         dhalf_pix = mm2pix(dhalf_mm)
  84         yoff = int(dhalf_pix / q)
  85     
  86     #a rectangle with a size proportional to the dc
  87     #it will be previewed and printed in the center of the physical page
  88     dc.SetBrush(wxBrush('#ffffff', wxTRANSPARENT))
  89     dc.SetPen(wxPen(wxBLUE))
  90     w, h = int(dcwi * 0.9), int(dche * 0.9)
  91     dc.DrawRectangle((dcwi / 2) - (w / 2) + xoff, (dche / 2) - (h / 2) + yoff, w, h)
  92     
  93     #some text, the font size is proportional to the physical page width
  94     c = 1000.0
  95     if orientation == 'portrait':
  96         PageA4PortraitWidth_pix = mm2pix(PageA4PortraitWidth_mm)
  97         fsu = PageA4PortraitWidth_pix / q / c
  98     else: #landscape
  99         PageA4LandscapeHeight_pix = mm2pix(PageA4LandscapeHeight_mm)
 100         fsu = PageA4LandscapeHeight_pix / q / c   
 101     fs = int(fsu * 20 + 0.5)
 102     if fs < 1.0: fs = 1.0
 103     facename = 'Courier New'
 104     dc.SetFont(wxFont(int(fs), wxMODERN, wxNORMAL, wxNORMAL, false, facename))
 105     s = '0---:----1----:----2----:----3----:----4----:----5'
 106     dc.DrawText(s, 0, 0)
 107         
 108     dc.EndDrawing()
 109 
 110 #-------------------------------------------------------------------
 111 
 112 #a dialog window for previewing
 113 #orientation : 'portrait' or 'landscape'
 114 #q: reduction factor, for printer q=1.0
 115 class ReducedPageA4(wxDialog):
 116     
 117     def __init__(self, parent, id, q, orientation):
 118         sty = wxDEFAULT_DIALOG_STYLE
 119         tit = 'Preview - reduced page A4 - %2i%% - HP DeskJet 500' % (1. / q * 100) 
 120         wxDialog.__init__(self, parent, id, tit, wxPoint(0, 0), wxSize(10, 10), style=sty)
 121         self.SetBackgroundColour('#dcdcdc')
 122         self.orientation = orientation
 123         self.q = q
 124 
 125         #my page A4
 126         if self.orientation == 'portrait':
 127             PageA4PortraitWidth_pix = mm2pix(PageA4PortraitWidth_mm)
 128             PageA4PortraitHeight_pix = mm2pix(PageA4PortraitHeight_mm)
 129             w = PageA4PortraitWidth_pix / self.q + DlgWidthCorr
 130             h = PageA4PortraitHeight_pix / self.q + DlgHeightCorr
 131         else: #landscape
 132             PageA4LandscapeWidth_pix = mm2pix(PageA4LandscapeWidth_mm)
 133             PageA4LandscapeHeight_pix = mm2pix(PageA4LandscapeHeight_mm)
 134             w = PageA4LandscapeWidth_pix / self.q + DlgWidthCorr
 135             h = PageA4LandscapeHeight_pix / self.q + DlgHeightCorr
 136         
 137         #set dialog sizes and center it
 138         self.SetSize((w, h))
 139         self.CenterOnScreen()
 140 
 141         #create the printable area window
 142         self.pa = wxWindow(self, id, wxPoint(0, 0), wxSize(100, 100))
 143         self.pa.SetBackgroundColour(wxWHITE)
 144         #size
 145         if self.orientation == 'portrait':
 146             w = PrintableAreaPortraitWidth_pix / self.q
 147             h = PrintableAreaPortraitHeight_pix / self.q
 148         else:
 149             w = PrintableAreaLandscapeWidth_pix / self.q
 150             h = PrintableAreaLandscapeHeight_pix / self.q
 151         self.pa.SetSize((w, h))
 152         
 153         self.pa.CenterOnParent()
 154         
 155         #position
 156         if self.orientation == 'portrait':
 157             NonPrintableMarginPortraitLeft_pix = mm2pix(NonPrintableMarginPortraitLeft_mm)
 158             NonPrintableMarginPortraitTop_pix = mm2pix(NonPrintableMarginPortraitTop_mm)
 159             le = NonPrintableMarginPortraitLeft_pix / self.q
 160             to = NonPrintableMarginPortraitTop_pix / self.q
 161         else:
 162             NonPrintableMarginLandscapeLeft_pix = mm2pix(NonPrintableMarginLandscapeLeft_mm)
 163             NonPrintableMarginLandscapeTop_pix = mm2pix(NonPrintableMarginLandscapeTop_mm)
 164             le = NonPrintableMarginLandscapeLeft_pix / self.q
 165             to = NonPrintableMarginLandscapeTop_pix / self.q
 166         self.pa.SetPosition((le, to))
 167 
 168         EVT_PAINT(self.pa, self.OnPaint)
 169         EVT_CHAR_HOOK(self, self.OnCharHook)
 170 
 171     def OnPaint(self, event):
 172         dc = wxPaintDC(self.pa)
 173         PrepareReducedPageA4Printout(dc, self.q, self.orientation)
 174 
 175     def OnCharHook(self, event):
 176         if event.KeyCode() == WXK_ESCAPE or event.KeyCode() == WXK_SPACE:
 177             self.EndModal(wxID_CANCEL)
 178         else:
 179             event.Skip()
 180 
 181 #-------------------------------------------------------------------
 182 
 183 #a printing matching the previewing
 184 class ReducedPageA4Printout(wxPrintout):
 185 
 186     def __init__(self, q, orientation):
 187         wxPrintout.__init__(self)
 188         self.orientation = orientation
 189         self.q = q
 190 
 191     def OnPrintPage(self, page):
 192         dc = self.GetDC()
 193         PrepareReducedPageA4Printout(dc, self.q, self.orientation)        
 194         return True
 195 
 196     def GetPageInfo(self):
 197         return (1, 1, 1, 1)
 198 
 199 #-------------------------------------------------------------------
 200 #-for zoom tests in preview-----------------------------------------
 201 #-------------------------------------------------------------------
 202 
 203 #a print job showing to be used in zoom/preview
 204 def PrepareStdPrintout(dc):
 205     dcwi, dche = dc.GetSizeTuple()
 206     dc.BeginDrawing()
 207     #a font propotional to the dc
 208     dcwi, dche = dc.GetSizeTuple()
 209     fsu = dcwi / 750.0
 210     fs = int(fsu * 30 + 0.5)
 211     if fs < 1: fs = 1
 212     facename = 'Courier New'
 213     dc.SetFont(wxFont(int(fs), wxMODERN, wxNORMAL, wxNORMAL, false, facename))
 214     s = '0---:----1----:----2----:----3----:----4----:----5'
 215     dc.DrawText(s, 0, 0)
 216     r = dc.GetFullTextExtent('M')
 217     for i in xrange(40):
 218         px, py = 0, r[1] * i
 219         dc.DrawText('line' + str(i), px, py)
 220     dc.EndDrawing()
 221     
 222 #-------------------------------------------------------------------
 223 
 224 class StdPrintout(wxPrintout):
 225 
 226     def __init__(self):
 227         wxPrintout.__init__(self)
 228 
 229     def OnPrintPage(self, page):
 230         dc = self.GetDC()
 231         PrepareStdPrintout(dc)
 232         return True
 233 
 234     def GetPageInfo(self):
 235         return (1, 1, 1, 1)
 236 
 237 #-------------------------------------------------------------------
 238 #-Main application--------------------------------------------------
 239 #-------------------------------------------------------------------
 240 
 241 class MyPanel(wxPanel):
 242 
 243     def __init__(self, parent):
 244         wxPanel.__init__(self, parent, -1, wxDefaultPosition, wxDefaultSize)
 245         self.parent = parent
 246         
 247         le = 10
 248         self.b1 = wxButton(self, 1001, 'Reduced page A4 (preview)', wxPoint(le, 10), wxDefaultSize)
 249         EVT_BUTTON(self, 1001, self.OnBut1)
 250         
 251         self.b2 = wxButton(self, 1002, 'Reduded page A4 (print)', wxPoint(le, 40), wxDefaultSize)
 252         EVT_BUTTON(self, 1002, self.OnBut2)
 253 
 254         self.b3 = wxButton(self, 1003, 'std wxPreview', wxPoint(le, 70), wxDefaultSize)
 255         EVT_BUTTON(self, 1003, self.OnBut3)
 256 
 257         self.b4 = wxButton(self, 1004, 'Quit', wxPoint(le, 100), wxDefaultSize)
 258         EVT_BUTTON(self, 1004, self.OnBut4)
 259 
 260     #a home build preview of the reduced page A4
 261     def OnBut1(self, event):
 262         dlg = ReducedPageA4(self, -1, 5, 'portrait')
 263         #~ dlg = ReducedPageA4(self, -1, 5, 'landscape')
 264         dlg.ShowModal()
 265         dlg.Destroy()
 266         
 267     #print the reduced page A4
 268     def OnBut2(self, event):
 269         pd = wxPrintData()
 270         pd.SetPaperId(wxPAPER_A4)
 271         pd.SetOrientation(wxPORTRAIT)
 272         #~ pd.SetOrientation(wxLANDSCAPE)
 273         pdd = wxPrintDialogData()
 274         pdd.SetPrintData(pd)
 275         printer = wxPrinter(pdd)
 276         printout = ReducedPageA4Printout(1.0, 'portrait')
 277         #~ printout = ReducedPageA4Printout(1.0, 'landscape')
 278         ret = printer.Print(self.parent, printout, false)
 279         if not ret:
 280             print 'Printer problem...'
 281         printout.Destroy()
 282 
 283     #standard wxPreview
 284     def OnBut3(self, event):
 285         pd = wxPrintData()
 286         pd.SetOrientation(wxPORTRAIT)
 287         printout = StdPrintout()
 288         printpreview = wxPrintPreview(printout, None, pd)
 289         printpreview.SetZoom(50)
 290         pos, size = wxPoint(0, 0), wxSize(700, 600)
 291         previewframe = wxPreviewFrame(printpreview, None, 'Std preview', pos, size)
 292         previewframe.Initialize()
 293         previewframe.Show()
 294         #do not destroy the printout object!
 295 
 296     def OnBut4(self, event):
 297         self.parent.Destroy()
 298 
 299 #-------------------------------------------------------------------
 300 
 301 class MyFrame(wxFrame):
 302 
 303     def __init__(self, parent, id):
 304         wxFrame.__init__(self, parent, id, "MyPrintPreview7", wxPoint(10,10), wxSize(400, 400))
 305         self.panel = MyPanel(self)
 306 
 307         EVT_CLOSE(self, self.OnCloseWindow)
 308 
 309     def OnCloseWindow(self, event):
 310         self.Destroy()
 311 
 312 #-------------------------------------------------------------------
 313 
 314 class MyApp(wxApp):
 315     
 316     def OnInit(self):
 317         frame = MyFrame(None, -1)
 318         frame.Show(True)
 319         self.SetTopWindow(frame)
 320         return True
 321 
 322 #-------------------------------------------------------------------
 323 
 324 def main():
 325     app = MyApp(0)
 326     app.MainLoop()
 327 
 328 #-------------------------------------------------------------------
 329 
 330 if __name__ == "__main__" :
 331     main()
 332     
 333 #eof-------------------------------------------------------------------

wx.HtmlPrintout without a setup dialog

wx.EasyHtmlPrinting is a handy class, but it doesn't (yet) support printing without first going through a dialog. You can do this with wx.HtmlPrintout, but it takes a (for me, nonobvious) step. Here's the code fragment I used in my application (where myHtmlText is the HTML you'd like to print):

  printout = wx.html.HtmlPrintout("foo")
  printout.SetHtmlText(myHtmlText)

  printData = wx.PrintData()
  printDialogData = wx.PrintDialogData()
  printDialogData.SetPrintData(printData)
  printDialogData.SetAllPages(True)
  printDialogData.SetNoCopies(1)
            
  printer = wx.Printer(printDialogData)
  printer.Print(None, printout, prompt=False)

What was nonobvious to me was having to call SetAllPages and SetNoCopies. Without these calls, under Windows I wound up a message claiming I had a job pending, but no jobs listed in the print queue; under OS X, I got a blank page. So, at the moment at least (wxPython 2.8.1.1), wx.PrintDialogData() does not necessarily default to sensible values.

Thanks to gpolo on the #wxWidgets IRC channel for helping me figure this out.

TedTurocy

Comments

MoreCommentsOnPrinting (last edited 2008-03-11 10:50:26 by localhost)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.