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.
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:
Finally, we can calculate font size for current DC:
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.
- The printable area and therefore the dc size may vary for different printers. A consequence is that the same document printed on different printers may not show the same font size.
- One more critical issue is printing a text of several lines. In wxPython, the font size must be an integer. After having calculated a theoretical font size and having truncated it, you may not print the same amount of lines on two different printers. This happens even if the size difference of the printable area on two different printers is small. I give here some numbers as example: the theoretical font size (based on the dc size) may be 11.9 for printer A, while it is 12.03 for printer B. In a real print, the printer A prints a document of with a font size of 11 and printer B uses a font size of 12. As consequence, each printer fill a page with a different number of lines.
Hmm... Maybe use of round(), instead of just int() conversion, can help here a little? If font size for printer A is 11.9, for printer B is 12.03, round() assures that for both printers font size will be 12
- The size may differ according to the orientation: portrait or landscape.
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:
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.
- It shows the font size issue.
- It proposes an home build preview window, which displays the physical page and its printable area.
- It shows how to print a rectangle centered on the physical page.
- It has a previewing and printingn, that match.
- User should adapt the constants to the used printer
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.