Canvas Creation

Skia has multiple backends which receive Canvas drawing commands, including:

  • Raster - CPU-only.

  • GPU - Skia’s GPU-accelerated backend.

  • PDF - PDF document creation.

  • Picture - Skia’s display list format.

  • SVG - Experimental SVG backend.

Each backend has a unique way of creating a Canvas. This page gives an example for each.

Raster

The raster backend draws to a block of memory. This memory can be managed by Skia or by the client.

The recommended way of creating a canvas for the Raster and Ganesh backends is to use a Surface, which is an object that manages the memory into which the canvas commands are drawn.

import skia

width, height = 200, 200
surface = skia.Surface(width, height)

with surface as canvas:
    canvas.drawCircle(100, 100, 40, skia.Paint(Color=skia.ColorGREEN))

image = surface.makeImageSnapshot()
image.save('output.png', skia.kPNG)

Alternatively, we could have specified the memory for the surface explicitly, instead of asking Skia to manage it. The following example directly draws into numpy array.

import skia
import numpy as np
import matplotlib.pyplot as plt

width, height = 200, 200
array = np.zeros((height, width, 4), dtype=np.uint8)

with skia.Surface(array) as canvas:
    canvas.drawCircle(100, 100, 40, skia.Paint(Color=skia.ColorGREEN))

plt.imshow(array)
plt.show()

GPU

GPU Surfaces must have a GrDirectContext object which manages the GPU context, and related caches for textures and fonts. GrDirectContexts are matched one to one with OpenGL contexts or Vulkan devices. That is, all Surface that will be rendered to using the same OpenGL context or Vulkan device should share a GrDirectContext. Skia does not create a OpenGL context or Vulkan device for you. In OpenGL mode it also assumes that the correct OpenGL context has been made current to the current thread when Skia calls are made.

The following example uses glfw package to create an OpenGL context. Install glfw via pip to try.

pip install glfw
import skia
import glfw
import contextlib


@contextlib.contextmanager
def glfw_context():
    if not glfw.init():
        raise RuntimeError('glfw.init() failed')
    glfw.window_hint(glfw.VISIBLE, glfw.FALSE)
    glfw.window_hint(glfw.STENCIL_BITS, 8)
    window = glfw.create_window(640, 480, '', None, None)
    glfw.make_context_current(window)
    yield window
    glfw.terminate()


width, height = 200, 200
with glfw_context():
    context = skia.GrDirectContext.MakeGL()
    info = skia.ImageInfo.MakeN32Premul(width, height)
    surface = skia.Surface.MakeRenderTarget(context, skia.Budgeted.kNo, info)
    assert surface is not None

    with surface as canvas:
        canvas.drawCircle(100, 100, 40, skia.Paint(Color=skia.ColorGREEN))

    image = surface.makeImageSnapshot()
    assert image is not None
    image.save('output.png', skia.kPNG)

OpenGL window

In the previous example, even though the rendering is executed on the GPU, nothing is actually shown on the screen. It is possible to use Skia to draw directly to a window on the screen. To do this:

  1. You have to use GLFW or other similar library to create and manage the window.

  2. GrBackendRenderTarget has to be created with a surface attached to it.

  3. The rendered image has to be flushed to the screen by calling Surface.flushAndSubmit().

Here’s a complete example:

import contextlib, glfw, skia
from OpenGL import GL

WIDTH, HEIGHT = 640, 480

@contextlib.contextmanager
def glfw_window():
    if not glfw.init():
        raise RuntimeError('glfw.init() failed')
    glfw.window_hint(glfw.STENCIL_BITS, 8)
    window = glfw.create_window(WIDTH, HEIGHT, '', None, None)
    glfw.make_context_current(window)
    yield window
    glfw.terminate()

@contextlib.contextmanager
def skia_surface(window):
    context = skia.GrDirectContext.MakeGL()
    (fb_width, fb_height) = glfw.get_framebuffer_size(window)
    backend_render_target = skia.GrBackendRenderTarget(
        fb_width,
        fb_height,
        0,  # sampleCnt
        0,  # stencilBits
        skia.GrGLFramebufferInfo(0, GL.GL_RGBA8))
    surface = skia.Surface.MakeFromBackendRenderTarget(
        context, backend_render_target, skia.kBottomLeft_GrSurfaceOrigin,
        skia.kRGBA_8888_ColorType, skia.ColorSpace.MakeSRGB())
    assert surface is not None
    yield surface
    context.abandonContext()

with glfw_window() as window:
    GL.glClear(GL.GL_COLOR_BUFFER_BIT)

    with skia_surface(window) as surface:
        with surface as canvas:
            canvas.drawCircle(100, 100, 40, skia.Paint(Color=skia.ColorGREEN))
        surface.flushAndSubmit()
        glfw.swap_buffers(window)

        while (glfw.get_key(window, glfw.KEY_ESCAPE) != glfw.PRESS
            and not glfw.window_should_close(window)):
            glfw.wait_events()

PDF

The PDF backend uses Document instead of Surface, since a document must include multiple pages. The following demonstrates how to write a single-page PDF document to a file:

import skia

width, height = 200, 200
stream = skia.FILEWStream('output.pdf')
with skia.PDF.MakeDocument(stream) as document:
    with document.page(width, height) as canvas:
        canvas.drawCircle(100, 100, 40, skia.Paint(Color=skia.ColorGREEN))

Picture

The Picture backend uses PictureRecorder instead of Surface.

import skia

width, height = 200, 200
recorder = skia.PictureRecorder()
canvas = recorder.beginRecording(width, height)
canvas.drawCircle(100, 100, 40, skia.Paint(Color=skia.ColorGREEN))
picture = recorder.finishRecordingAsPicture()
data = picture.serialize()

Recorded picture can later be drawn onto another canvas:

picture.playback(canvas)

SVG

The (still experimental) SVGCanvas writes into an SVG document.

import skia

width, height = 200, 200
stream = skia.FILEWStream('output.svg')
canvas = skia.SVGCanvas.Make((width, height), stream)
draw(canvas)
del canvas
stream.flush()