= How to use the Cairo 2D graphics library (Phoenix) =
'''Keywords :''' Drawing, Cairo, BufferedPaintDC, Bitmap, Gradient.

<<TableOfContents>>

--------
= Introduction : =
Shows how to draw on a DC using the cairo 2D graphics library and either the PyCairo or cairocffi package which wrap the cairo API.

--------
= Demonstrating : =
__'''''Tested''' py3.x, wx4.x and Win10. ''__

Are you ready to use some samples ? ;)

Test, modify, correct, complete, improve and share your discoveries ! (!)

--------
== Sample one ==
You must install this package for use it :

'''pip install cairocffi '''

or

'''pip install pycairo '''

{{attachment:img_sample_one.png}}

Available with wxPython demo.

{{{#!python
# sample_one.py

import os
import wx
import math

try:
    import wx.lib.wxcairo as wxcairo
    import cairo
    haveCairo = True
except ImportError:
    haveCairo = False

# def opj
# class MyPanel
# class MyFrame
# class MyApp

#----------------------------------------------------------------------

def opj(path):
    """
    Convert paths to the platform-specific separator.
    """

    st = os.path.join(*tuple(path.split('/')))
    # HACK: on Linux, a leading / gets lost...
    if path.startswith('/'):
        st = '/' + st
    return st

#----------------------------------------------------------------------

class MyPanel(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent, -1)

        self.Bind(wx.EVT_PAINT, self.OnPaint)

    #-----------------------------------------------------------------------

    def OnPaint(self, evt):
        """
        ...
        """

        if self.IsDoubleBuffered():
            dc = wx.PaintDC(self)
        else:
            dc = wx.BufferedPaintDC(self)
        dc.SetBackground(wx.WHITE_BRUSH)
        dc.Clear()

        self.Render(dc)


    def Render(self, dc):
        """
        ...
        """

        # Draw some stuff on the plain dc.
        sz = self.GetSize()
        dc.SetPen(wx.Pen("navy", 1))

        x = y = 0
        while x < sz.width * 2 or y < sz.height * 2:
            x += 20
            y += 20
            dc.DrawLine(x, 0, 0, y)

        # Now draw something with cairo.
        ctx = wxcairo.ContextFromDC(dc)
        ctx.set_line_width(15)
        ctx.move_to(125, 25)
        ctx.line_to(225, 225)
        ctx.rel_line_to(-200, 0)
        ctx.close_path()
        ctx.set_source_rgba(0, 0, 0.5, 1)
        ctx.stroke()

        # And something else...
        ctx.arc(200, 200, 80, 0, math.pi*2)
        ctx.set_source_rgba(0, 1, 1, 0.5)
        ctx.fill_preserve()
        ctx.set_source_rgb(1, 0.5, 0)
        ctx.stroke()

        # Here's a gradient pattern.
        ptn = cairo.RadialGradient(315, 70, 25,
                                   302, 70, 128)
        ptn.add_color_stop_rgba(0, 1,1,1,1)
        ptn.add_color_stop_rgba(1, 0,0,0,1)
        ctx.set_source(ptn)
        ctx.arc(328, 96, 75, 0, math.pi*2)
        ctx.fill()

        # Draw some text.
        face = wxcairo.FontFaceFromFont(
            wx.FFont(10, wx.FONTFAMILY_SWISS, wx.FONTFLAG_BOLD))
        ctx.set_font_face(face)
        ctx.set_font_size(60)
        ctx.move_to(360, 180)
        ctx.set_source_rgb(0, 0, 0)
        ctx.show_text("Hello")

        # Text as a path, with fill and stroke.
        ctx.move_to(400, 220)
        ctx.text_path("World")
        ctx.set_source_rgb(0.39, 0.07, 0.78)
        ctx.fill_preserve()
        ctx.set_source_rgb(0,0,0)
        ctx.set_line_width(2)
        ctx.stroke()

        # Show iterating and modifying a (text) path.
        ctx.new_path()
        ctx.move_to(0, 0)
        ctx.set_source_rgb(0.3, 0.3, 0.3)
        ctx.set_font_size(30)
        text = "This path was warped..."
        ctx.text_path(text)
        tw, th = ctx.text_extents(text)[2:4]
        self.warpPath(ctx, tw, th, 360,300)
        ctx.fill()

        # Drawing a bitmap.  Note that we can easily load a PNG file
        # into a surface, but I wanted to show how to convert from a
        # wx.Bitmap here instead.  This is how to do it using just cairo :
        #img = cairo.ImageSurface.create_from_png(opj('bitmaps/toucan.png'))

        # And this is how to convert a wx.Btmap to a cairo image
        # surface.  NOTE: currently on Mac there appears to be a
        # problem using conversions of some types of images.  They
        # show up totally transparent when used. The conversion itself
        # appears to be working okay, because converting back to
        # wx.Bitmap or writing the image surface to a file produces
        # the expected result.  The other platforms are okay.
        bmp = wx.Bitmap(opj('bitmaps/toucan.png'))
        img = wxcairo.ImageSurfaceFromBitmap(bmp)

        ctx.set_source_surface(img, 70, 230)
        ctx.paint()

        # This is how to convert an image surface to a wx.Bitmap.
        bmp2 = wxcairo.BitmapFromImageSurface(img)
        dc.DrawBitmap(bmp2, 280, 300)


    def warpPath(self, ctx, tw, th, dx, dy):
        """
        ...
        """

        def f(x, y):
            xn = x - tw/2
            yn = y+ xn ** 3 / ((tw/2)**3) * 70
            return xn+dx, yn+dy

        path = ctx.copy_path()

        ctx.new_path()
        for type, points in path:
            if type == cairo.PATH_MOVE_TO:
                x, y = f(*points)
                ctx.move_to(x, y)

            elif type == cairo.PATH_LINE_TO:
                x, y = f(*points)
                ctx.line_to(x, y)

            elif type == cairo.PATH_CURVE_TO:
                x1, y1, x2, y2, x3, y3 = points
                x1, y1 = f(x1, y1)
                x2, y2 = f(x2, y2)
                x3, y3 = f(x3, y3)
                ctx.curve_to(x1, y1, x2, y2, x3, y3)

            elif type == cairo.PATH_CLOSE_PATH:
                ctx.close_path()

#---------------------------------------------------------------------------

class MyFrame(wx.Frame):
    """
    ...
    """
    def __init__(self):
        super(MyFrame, self).__init__(None,
                                      -1,
                                      title="Sample_one")

        #------------

        # Simplified init method.
        self.SetProperties()
        self.CreateCtrls()
        self.BindEvents()
        self.DoLayout()

        #------------

        self.CenterOnScreen()

    #-----------------------------------------------------------------------

    def SetProperties(self):
        """
        ...
        """

        self.SetMinSize((600, 420))

        #------------

        frameicon = wx.Icon("wxwin.ico")
        self.SetIcon(frameicon)


    def CreateCtrls(self):
        """
        ...
        """

        # Create a panel.
        self.panel = MyPanel(self)


    def BindEvents(self):
        """
        Bind some events to an events handler.
        """

        # Bind the close event to an event handler.
        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)


    def DoLayout(self):
        """
        ...
        """

        # MainSizer is the top-level one that manages everything.
        mainSizer = wx.BoxSizer(wx.VERTICAL)

        # Finally, tell the panel to use the sizer for layout.
        self.panel.SetAutoLayout(True)
        self.panel.SetSizer(mainSizer)

        mainSizer.Fit(self.panel)

    #-----------------------------------------------------------------------

    def OnCloseMe(self, event):
        """
        ...
        """

        self.Close(True)


    def OnCloseWindow(self, event):
        """
        ...
        """

        self.Destroy()

#---------------------------------------------------------------------------

class MyApp(wx.App):
    """
    ...
    """
    def OnInit(self):

        #------------

        frame = MyFrame()
        self.SetTopWindow(frame)
        frame.CenterOnScreen(wx.BOTH)
        frame.Show(True)

        return True

#---------------------------------------------------------------------------

def main():
    app = MyApp(redirect=False)
    app.MainLoop()

#---------------------------------------------------------------------------

if __name__ == "__main__" :
    main()
}}}
--------
== Sample two ==
{{attachment:img_sample_two.png}}

{{{#!python
# sample_two.py

import wx

try:
    import wx.lib.wxcairo as wxcairo
    import cairo
    haveCairo = True
except ImportError:
    haveCairo = False

# class MyPanel
# class MyFrame
# class MyApp


"""
We want the font to be dynamically sized so the text fits.
Resizing the window should dynamically resize the text, but
this should only happen if the text is not already at a
width or height constraint.
"""

#-------------------------------------------------------------------------------

class MyPanel(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent, style=wx.BORDER_SIMPLE)

        #------------

        self.text = "Hello World !"

        #------------

        self.Bind(wx.EVT_SIZE, self.OnResize)
        self.Bind(wx.EVT_PAINT, self.OnPaint)

    #---------------------------------------------------------------------------

    def OnPaint(self, evt):
        """
        ...
        """

        # Here we do some magic WX stuff.
        dc = wx.BufferedPaintDC(self)
        width, height = self.GetClientSize()
        cr = wx.lib.wxcairo.ContextFromDC(dc)

        # Here's actual Cairo drawing.
        size = min(width, height)
        cr.scale(size, size)
        cr.set_source_rgb(0, 0, 0) # black
        cr.rectangle(0, 0, width, height)
        cr.fill()

        cr.set_source_rgb(1, 1, 1) # white
        cr.set_line_width (0.04)
        cr.select_font_face ("Sans", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
        cr.set_font_size (0.07)
        cr.move_to (0.5, 0.5)
        cr.show_text (self.text)
        cr.stroke ()


    def SetText(self, text):
        """
        ...
        """

        # Change what text is shown.
        self.text = text
        self.Refresh()


    def OnResize(self, event):
        """
        ...
        """

        self.Refresh()
        self.Layout()

#-------------------------------------------------------------------------------

class MyFrame(wx.Frame):
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(320, 250))

        self.SetMinSize((320, 250))

        frameicon = wx.Icon("wxwin.ico")
        self.SetIcon(frameicon)

        #------------

        self.canvas = MyPanel(self)

        self.Show()

#-------------------------------------------------------------------------------

class MyApp(wx.App):
    def OnInit(self):

        #------------

        frame = MyFrame(None, "Sample_two")
        self.SetTopWindow(frame)
        frame.Show(True)

        return True

#-------------------------------------------------------------------------------

def main():
    app = MyApp(False)
    app.MainLoop()

#-------------------------------------------------------------------------------

if __name__ == "__main__" :
    main()
}}}
--------
== Sample three ==
{{attachment:img_sample_three.png}}

{{{#!python
# sample_three.py

import wx
import math

try:
    import wx.lib.wxcairo as wxcairo
    import cairo
    haveCairo = True
except ImportError:
    haveCairo = False

# class MyPanel
# class MyFrame
# class MyApp

#---------------------------------------------------------------------------

class MyPanel(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent, -1)

        self.Bind(wx.EVT_PAINT, self.OnPaint)

    #-----------------------------------------------------------------------

    def OnPaint(self, event):
        """
        ...
        """

        if self.IsDoubleBuffered():
            dc = wx.PaintDC(self)
        else:
            dc = wx.BufferedPaintDC(self)
        dc.SetBackground(wx.WHITE_BRUSH)
        dc.Clear()

        self.Render(dc)


    def Render(self, dc):
        """
        ...
        """

        # Now draw something with cairo.
        ctx = wxcairo.ContextFromDC(dc)

        # Drawing a bitmap.  Note that we can easily load a PNG file
        # into a surface, but I wanted to show how to convert from a
        # wx.Bitmap here instead.  This is how to do it using just cairo :
        # img = cairo.ImageSurface.create_from_png(opj('bitmaps/toucan.png'))

        # And this is how to convert a wx.Bitmap to a cairo image
        # surface.  NOTE : currently on Mac there appears to be a
        # problem using conversions of some types of images.  They
        # show up totally transparent when used. The conversion itself
        # appears to be working okay, because converting back to
        # wx.Bitmap or writing the image surface to a file produces
        # the expected result.  The other platforms are okay.
        bmp = wx.Bitmap('fruit.jpg')
        img = wxcairo.ImageSurfaceFromBitmap(bmp)

        ctx.set_source_surface(img, 50, 50)
        ctx.paint()

        # This is how to convert an image surface to a wx.Bitmap.
        bmp2 = wxcairo.BitmapFromImageSurface(img)
        dc.DrawBitmap(bmp2, 100, 100)

        img.write_to_png('img_sample_three.png')

#---------------------------------------------------------------------------

class MyFrame(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, -1, title,
                          style=wx.DEFAULT_FRAME_STYLE)

        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)

        #------------

        self.SetIcon(wx.Icon('wxwin.ico'))
        self.SetInitialSize((330, 330))

        #------------

        # Attributes
        self.panel= MyPanel(self)

        # Layout
        self.DoLayout()

    #-----------------------------------------------------------------------

    def DoLayout(self):
        """
        ...
        """

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)


    def OnCloseWindow(self, event):
        """
        ...
        """

        self.Destroy()

#---------------------------------------------------------------------------

class MyApp(wx.App):
    def OnInit(self):

        #------------

        frame = MyFrame(None, -1, "Sample three (image)")
        self.SetTopWindow(frame)
        frame.Show(True)

        return True

#---------------------------------------------------------------------------

def main():
    app = MyApp(False)
    app.MainLoop()

#---------------------------------------------------------------------------


if __name__ == "__main__" :
    main()
}}}
--------
== Sample four ==
{{attachment:img_sample_four.png}}

{{{#!python
# sample_four.py

import wx
import math

try:
    import wx.lib.wxcairo as wxcairo
    import cairo
    haveCairo = True
except ImportError:
    haveCairo = False

# class MyPanel
# class MyFrame
# class MyApp

#---------------------------------------------------------------------------

class MyPanel(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent, -1, style=wx.WANTS_CHARS)

        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.Bind(wx.EVT_PAINT, self.OnPaint)

    #-----------------------------------------------------------------------

    def OnKeyDown(self, event):
        """
        ...
        """

        keycode = event.GetKeyCode()

        if keycode == wx.WXK_F12:
            self.OnSave()
        else:
            event.Skip()


    def OnPaint(self, event):
        """
        ...
        """

        if self.IsDoubleBuffered():
            dc = wx.PaintDC(self)
        else:
            dc = wx.BufferedPaintDC(self)
        dc.SetBackground(wx.YELLOW_BRUSH)
        dc.Clear()

        self.Render(dc)


    def Render(self, dc):
        """
        ...
        """

        # Now draw something with cairo.
        ctx = wxcairo.ContextFromDC(dc)

        # Drawing a bitmap.  Note that we can easily load a PNG file
        # into a surface, but I wanted to show how to convert from a
        # wx.Bitmap here instead.  This is how to do it using just cairo :
        # img = cairo.ImageSurface.create_from_png(opj('bitmaps/toucan.png'))

        # And this is how to convert a wx.Bitmap to a cairo image
        # surface.  NOTE : currently on Mac there appears to be a
        # problem using conversions of some types of images.  They
        # show up totally transparent when used. The conversion itself
        # appears to be working okay, because converting back to
        # wx.Bitmap or writing the image surface to a file produces
        # the expected result.  The other platforms are okay.
        bmp = wx.Bitmap("guidoy.png")
        self.img = wxcairo.ImageSurfaceFromBitmap(bmp)

        x = 20
        y = 20
        w = 275
        h = 250
        r = 20

        ctx.move_to(x+r, y)
        ctx.line_to(x+w-r-1, y)
        ctx.arc(x+w-r-1, y+r, r, -0.5*math.pi, 0)
        ctx.line_to(x+w-1, y+h-r-1)
        ctx.arc(x+w-r-1, y+h-r-1, r, 0, 0.5*math.pi)
        ctx.line_to(x+r, y+h-1)
        ctx.arc(x+r, y+h-r-1, r, 0.5*math.pi, math.pi)
        ctx.line_to(x, y+r)
        ctx.arc(x+r, y+r, r, math.pi, 1.5*math.pi)
        ctx.close_path()
        ctx.clip()

        ctx.set_source_surface(self.img, 0, 0)
        ctx.paint()


    def OnSave(self):
        """
        ...
        """

        self.img.write_to_png('img_sample_four.png')

#---------------------------------------------------------------------------

class MyFrame(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, -1, title,
                          style=wx.DEFAULT_FRAME_STYLE)

        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)

        #------------

        self.SetIcon(wx.Icon('wxwin.ico'))
        self.SetInitialSize((330, 330))

        #------------

        # Attributes
        self.panel= MyPanel(self)

        # Layout
        self.DoLayout()

    #-----------------------------------------------------------------------

    def DoLayout(self):
        """
        ...
        """

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)


    def OnCloseWindow(self, event):
        """
        ...
        """

        self.Destroy()

#---------------------------------------------------------------------------

class MyApp(wx.App):
    def OnInit(self):

        #------------

        frame = MyFrame(None, -1, "Sample four (clip image)")
        self.SetTopWindow(frame)
        frame.Show(True)
        return True

#---------------------------------------------------------------------------

def main():
    app = MyApp(False)
    app.MainLoop()

#---------------------------------------------------------------------------

if __name__ == "__main__" :
    main()
}}}
--------
= Download source =
[[attachment:source.zip]]

--------
= Additional Information =
'''Link :'''

https://www.cairographics.org/documentation/

https://www.cairographics.org/samples/

https://www.cairographics.org/cookbook/

https://www.cairographics.org/tutorial/

https://www.cairographics.org/manual/

http://www.tortall.net/mu/wiki/CairoTutorial

https://stackoverflow.com/questions/23661347/drawing-with-cairo-in-wxpython

- - - - -

https://wiki.wxpython.org/TitleIndex

https://docs.wxpython.org/

--------
= Thanks to =
Robin Dunn (sample_one.py coding), ??? (sample_two.py coding), the wxPython community...

--------
= About this page =
Date(d/m/y)     Person (bot)    Comments :

12/01/20 - Ecco (Updated page for wxPython Phoenix).

--------
= Comments =
- blah, blah, blah....