=== 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 [[http://lists.wxwidgets.org/archive/wxPython-users/thrd41.html#02588|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. {{{ #!python 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. {{{ #!python fs = int(fsu * 10 + 0.5) if fs < 1: fs = 1 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: {{{ #!python # device context page width in pixels if wxPrintout.IsPreview(): w_px = wxPrintout.GetDC().GetSizeTuple()[0] else: w_px = wxPrintout.GetPageSizePixels()[0] # number of milimeters per inch mpi = 25.4 # physical page width in mm w_mm = wxPrintout.GetPageSizeMM()[0] 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: {{{ #!python # dc_ppi - device context page pixel per inch resolution if wxPrintout.IsPreview(): dc_ppi = self.GetPPIScreen()[0] else: dc_ppi = self.GetPPIPrinter()[0] scale_factor = page_ppi / dc_ppi }}} Finally, we can calculate font size for current DC: {{{ #!python # font size in points (like in word processor - you know ;) des_font_size = 12 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): {{{ #!python class myPrintout(wxPrintout): # (...) def OnPrintPage(self, page): # (...) if self.IsPreview(): dc_ppi = self.GetPPIScreen()[0] w_px = self.GetDC().GetSizeTuple()[0] else: dc_ppi = self.GetPPIPrinter()[0] w_px = self.GetPageSizePixels()[0] w_mm = self.GetPageSizeMM()[0] my_fs = 30 # desirable font size in points (1/72 of inch) mpi = 25.4 fs = round((my_fs * w_px * mpi) / (w_mm * dc_ppi)) # put some font initialization here with calculated font size (fs) }}} I hope this will help somebody :) greetz from [[mailto:sototh(at)gts.pl|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: {{{ #!python NonPrintableMargins = {'None': (,), \ 'HP Deskjet 500': (3.2, 6.3, 3.6, 12.8), \ '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. * 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 {{{ #!python #------------------------------------------------------------------- #-*- coding: iso-8859-1 -*- #------------------------------------------------------------------- # MyPrintPreview7.py # wxPython 2.4.2.4, Python 2.3.0, win 98 # Jean-Michel Fauth, Switzerland # 22 October 2003 #------------------------------------------------------------------- from wxPython.wx import * #------------------------------------------------------------------- #constants DlgHeightCorr = 25 #caption bar + borders for default style, pixels DlgWidthCorr = 6 #borders for default style, pixels PageA4PortraitWidth_mm = 210.0 PageA4PortraitHeight_mm = 297.0 PageA4LandscapeWidth_mm = 297.0 PageA4LandscapeHeight_mm = 210.0 #for my hp deskjet 500, numbers from wxPython/printout PrintableAreaPortraitWidth_pix = 2400.0 PrintableAreaPortraitHeight_pix = 3282.0 PrintableAreaPortraitWidth_mm = 203.0 PrintableAreaPortraitHeight_mm = 278.0 PrintableAreaLandscapeWidth_pix = 3282.0 PrintableAreaLandscapeHeight_pix = 2400.0 PrintableAreaLandscapeWidth_mm = 278.0 PrintableAreaLandscapeHeight_mm = 203.0 #for my hp deskjet 500, numbers from other tools eg word NonPrintableMarginPortraitLeft_mm = 3.2 NonPrintableMarginPortraitTop_mm = 6.3 NonPrintableMarginPortraitRight_mm = 3.6 NonPrintableMarginPortraitBottom_mm = 12.8 NonPrintableMarginLandscapeLeft_mm = 6.3 NonPrintableMarginLandscapeTop_mm = 3.6 NonPrintableMarginLandscapeRight_mm = 12.8 NonPrintableMarginLandscapeBottom_mm = 3.2 #------------------------------------------------------------------- #convert mm into pixels (float) def mm2pix(mm): return mm / PrintableAreaPortraitWidth_mm * PrintableAreaPortraitWidth_pix #------------------------------------------------------------------- #a dc independent print job, used for printing and previewing def PrepareReducedPageA4Printout(dc, q, orientation): dcwi, dche = dc.GetSizeTuple() dc.BeginDrawing() #rectangle on dc limits dc.SetBrush(wxBrush('#ffffff', wxTRANSPARENT )) dc.SetPen(wxPen(wxRED)) dc.DrawRectangle(0, 0, dcwi, dche) #a square centered on the page #offsets if orientation == 'portrait': #margin right > margin left ! dhalf_mm = (NonPrintableMarginPortraitRight_mm - NonPrintableMarginPortraitLeft_mm) / 2.0 dhalf_pix = mm2pix(dhalf_mm) xoff = int(dhalf_pix / q) #margin bottom > margin top ! dhalf_mm = (NonPrintableMarginPortraitBottom_mm - NonPrintableMarginPortraitTop_mm) / 2.0 dhalf_pix = mm2pix(dhalf_mm) yoff = int(dhalf_pix / q) print 'q:', q print 'x,y off:', xoff, yoff else: #landscape #margin right > margin left ! dhalf_mm = (NonPrintableMarginLandscapeRight_mm - NonPrintableMarginLandscapeLeft_mm) / 2.0 dhalf_pix = mm2pix(dhalf_mm) xoff = int(dhalf_pix / q) #margin top > margin bottom ! dhalf_mm = (NonPrintableMarginLandscapeTop_mm - NonPrintableMarginLandscapeBottom_mm) / 2.0 dhalf_pix = mm2pix(dhalf_mm) yoff = int(dhalf_pix / q) #a rectangle with a size proportional to the dc #it will be previewed and printed in the center of the physical page dc.SetBrush(wxBrush('#ffffff', wxTRANSPARENT)) dc.SetPen(wxPen(wxBLUE)) w, h = int(dcwi * 0.9), int(dche * 0.9) dc.DrawRectangle((dcwi / 2) - (w / 2) + xoff, (dche / 2) - (h / 2) + yoff, w, h) #some text, the font size is proportional to the physical page width c = 1000.0 if orientation == 'portrait': PageA4PortraitWidth_pix = mm2pix(PageA4PortraitWidth_mm) fsu = PageA4PortraitWidth_pix / q / c else: #landscape PageA4LandscapeHeight_pix = mm2pix(PageA4LandscapeHeight_mm) fsu = PageA4LandscapeHeight_pix / q / c fs = int(fsu * 20 + 0.5) if fs < 1.0: fs = 1.0 facename = 'Courier New' dc.SetFont(wxFont(int(fs), wxMODERN, wxNORMAL, wxNORMAL, false, facename)) s = '0---:----1----:----2----:----3----:----4----:----5' dc.DrawText(s, 0, 0) dc.EndDrawing() #------------------------------------------------------------------- #a dialog window for previewing #orientation : 'portrait' or 'landscape' #q: reduction factor, for printer q=1.0 class ReducedPageA4(wxDialog): def __init__(self, parent, id, q, orientation): sty = wxDEFAULT_DIALOG_STYLE tit = 'Preview - reduced page A4 - %2i%% - HP DeskJet 500' % (1. / q * 100) wxDialog.__init__(self, parent, id, tit, wxPoint(0, 0), wxSize(10, 10), style=sty) self.SetBackgroundColour('#dcdcdc') self.orientation = orientation self.q = q #my page A4 if self.orientation == 'portrait': PageA4PortraitWidth_pix = mm2pix(PageA4PortraitWidth_mm) PageA4PortraitHeight_pix = mm2pix(PageA4PortraitHeight_mm) w = PageA4PortraitWidth_pix / self.q + DlgWidthCorr h = PageA4PortraitHeight_pix / self.q + DlgHeightCorr else: #landscape PageA4LandscapeWidth_pix = mm2pix(PageA4LandscapeWidth_mm) PageA4LandscapeHeight_pix = mm2pix(PageA4LandscapeHeight_mm) w = PageA4LandscapeWidth_pix / self.q + DlgWidthCorr h = PageA4LandscapeHeight_pix / self.q + DlgHeightCorr #set dialog sizes and center it self.SetSize((w, h)) self.CenterOnScreen() #create the printable area window self.pa = wxWindow(self, id, wxPoint(0, 0), wxSize(100, 100)) self.pa.SetBackgroundColour(wxWHITE) #size if self.orientation == 'portrait': w = PrintableAreaPortraitWidth_pix / self.q h = PrintableAreaPortraitHeight_pix / self.q else: w = PrintableAreaLandscapeWidth_pix / self.q h = PrintableAreaLandscapeHeight_pix / self.q self.pa.SetSize((w, h)) self.pa.CenterOnParent() #position if self.orientation == 'portrait': NonPrintableMarginPortraitLeft_pix = mm2pix(NonPrintableMarginPortraitLeft_mm) NonPrintableMarginPortraitTop_pix = mm2pix(NonPrintableMarginPortraitTop_mm) le = NonPrintableMarginPortraitLeft_pix / self.q to = NonPrintableMarginPortraitTop_pix / self.q else: NonPrintableMarginLandscapeLeft_pix = mm2pix(NonPrintableMarginLandscapeLeft_mm) NonPrintableMarginLandscapeTop_pix = mm2pix(NonPrintableMarginLandscapeTop_mm) le = NonPrintableMarginLandscapeLeft_pix / self.q to = NonPrintableMarginLandscapeTop_pix / self.q self.pa.SetPosition((le, to)) EVT_PAINT(self.pa, self.OnPaint) EVT_CHAR_HOOK(self, self.OnCharHook) def OnPaint(self, event): dc = wxPaintDC(self.pa) PrepareReducedPageA4Printout(dc, self.q, self.orientation) def OnCharHook(self, event): if event.KeyCode() == WXK_ESCAPE or event.KeyCode() == WXK_SPACE: self.EndModal(wxID_CANCEL) else: event.Skip() #------------------------------------------------------------------- #a printing matching the previewing class ReducedPageA4Printout(wxPrintout): def __init__(self, q, orientation): wxPrintout.__init__(self) self.orientation = orientation self.q = q def OnPrintPage(self, page): dc = self.GetDC() PrepareReducedPageA4Printout(dc, self.q, self.orientation) return True def GetPageInfo(self): return (1, 1, 1, 1) #------------------------------------------------------------------- #-for zoom tests in preview----------------------------------------- #------------------------------------------------------------------- #a print job showing to be used in zoom/preview def PrepareStdPrintout(dc): dcwi, dche = dc.GetSizeTuple() dc.BeginDrawing() #a font propotional to the dc dcwi, dche = dc.GetSizeTuple() fsu = dcwi / 750.0 fs = int(fsu * 30 + 0.5) if fs < 1: fs = 1 facename = 'Courier New' dc.SetFont(wxFont(int(fs), wxMODERN, wxNORMAL, wxNORMAL, false, facename)) s = '0---:----1----:----2----:----3----:----4----:----5' dc.DrawText(s, 0, 0) r = dc.GetFullTextExtent('M') for i in xrange(40): px, py = 0, r[1] * i dc.DrawText('line' + str(i), px, py) dc.EndDrawing() #------------------------------------------------------------------- class StdPrintout(wxPrintout): def __init__(self): wxPrintout.__init__(self) def OnPrintPage(self, page): dc = self.GetDC() PrepareStdPrintout(dc) return True def GetPageInfo(self): return (1, 1, 1, 1) #------------------------------------------------------------------- #-Main application-------------------------------------------------- #------------------------------------------------------------------- class MyPanel(wxPanel): def __init__(self, parent): wxPanel.__init__(self, parent, -1, wxDefaultPosition, wxDefaultSize) self.parent = parent le = 10 self.b1 = wxButton(self, 1001, 'Reduced page A4 (preview)', wxPoint(le, 10), wxDefaultSize) EVT_BUTTON(self, 1001, self.OnBut1) self.b2 = wxButton(self, 1002, 'Reduded page A4 (print)', wxPoint(le, 40), wxDefaultSize) EVT_BUTTON(self, 1002, self.OnBut2) self.b3 = wxButton(self, 1003, 'std wxPreview', wxPoint(le, 70), wxDefaultSize) EVT_BUTTON(self, 1003, self.OnBut3) self.b4 = wxButton(self, 1004, 'Quit', wxPoint(le, 100), wxDefaultSize) EVT_BUTTON(self, 1004, self.OnBut4) #a home build preview of the reduced page A4 def OnBut1(self, event): dlg = ReducedPageA4(self, -1, 5, 'portrait') #~ dlg = ReducedPageA4(self, -1, 5, 'landscape') dlg.ShowModal() dlg.Destroy() #print the reduced page A4 def OnBut2(self, event): pd = wxPrintData() pd.SetPaperId(wxPAPER_A4) pd.SetOrientation(wxPORTRAIT) #~ pd.SetOrientation(wxLANDSCAPE) pdd = wxPrintDialogData() pdd.SetPrintData(pd) printer = wxPrinter(pdd) printout = ReducedPageA4Printout(1.0, 'portrait') #~ printout = ReducedPageA4Printout(1.0, 'landscape') ret = printer.Print(self.parent, printout, false) if not ret: print 'Printer problem...' printout.Destroy() #standard wxPreview def OnBut3(self, event): pd = wxPrintData() pd.SetOrientation(wxPORTRAIT) printout = StdPrintout() printpreview = wxPrintPreview(printout, None, pd) printpreview.SetZoom(50) pos, size = wxPoint(0, 0), wxSize(700, 600) previewframe = wxPreviewFrame(printpreview, None, 'Std preview', pos, size) previewframe.Initialize() previewframe.Show() #do not destroy the printout object! def OnBut4(self, event): self.parent.Destroy() #------------------------------------------------------------------- class MyFrame(wxFrame): def __init__(self, parent, id): wxFrame.__init__(self, parent, id, "MyPrintPreview7", wxPoint(10,10), wxSize(400, 400)) self.panel = MyPanel(self) EVT_CLOSE(self, self.OnCloseWindow) def OnCloseWindow(self, event): self.Destroy() #------------------------------------------------------------------- class MyApp(wxApp): def OnInit(self): frame = MyFrame(None, -1) frame.Show(True) self.SetTopWindow(frame) return True #------------------------------------------------------------------- def main(): app = MyApp(0) app.MainLoop() #------------------------------------------------------------------- if __name__ == "__main__" : main() #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 ===