=== 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 ===