Add existing to tracked
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
cairocffi
|
||||
~~~~~~~~~
|
||||
|
||||
CFFI-based cairo bindings for Python. See README for details.
|
||||
|
||||
:copyright: Copyright 2013-2019 by Simon Sapin
|
||||
:license: BSD, see LICENSE for details.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from ctypes.util import find_library
|
||||
|
||||
from . import constants
|
||||
from .ffi import ffi
|
||||
|
||||
VERSION = __version__ = '1.7.1'
|
||||
# supported version of cairo, used to be pycairo version too:
|
||||
version = '1.17.2'
|
||||
version_info = (1, 17, 2)
|
||||
|
||||
|
||||
# Python 3.8 no longer searches for DLLs in PATH, so we can add everything in
|
||||
# CAIROCFFI_DLL_DIRECTORIES manually. Note that unlike PATH, add_dll_directory
|
||||
# has no defined order, so if there are two cairo DLLs in PATH we might get a
|
||||
# random one.
|
||||
dll_directories = os.getenv('CAIROCFFI_DLL_DIRECTORIES')
|
||||
if dll_directories and hasattr(os, 'add_dll_directory'):
|
||||
for path in dll_directories.split(';'):
|
||||
with suppress((OSError, FileNotFoundError)):
|
||||
os.add_dll_directory(path)
|
||||
|
||||
|
||||
def dlopen(ffi, library_names, filenames):
|
||||
"""Try various names for the same library, for different platforms."""
|
||||
exceptions = []
|
||||
|
||||
for library_name in library_names:
|
||||
library_filename = find_library(library_name)
|
||||
if library_filename:
|
||||
filenames = (library_filename, *filenames)
|
||||
else:
|
||||
exceptions.append(
|
||||
'no library called "{}" was found'.format(library_name))
|
||||
|
||||
for filename in filenames:
|
||||
try:
|
||||
return ffi.dlopen(filename)
|
||||
except OSError as exception: # pragma: no cover
|
||||
exceptions.append(exception)
|
||||
|
||||
error_message = '\n'.join( # pragma: no cover
|
||||
str(exception) for exception in exceptions)
|
||||
raise OSError(error_message) # pragma: no cover
|
||||
|
||||
|
||||
cairo = dlopen(
|
||||
ffi, ('cairo-2', 'cairo', 'libcairo-2'),
|
||||
('libcairo.so.2', 'libcairo.2.dylib', 'libcairo-2.dll'))
|
||||
|
||||
|
||||
class _keepref(object): # noqa: N801
|
||||
"""Function wrapper that keeps a reference to another object."""
|
||||
def __init__(self, ref, func):
|
||||
self.ref = ref
|
||||
self.func = func
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.func(*args, **kwargs)
|
||||
|
||||
|
||||
class CairoError(Exception):
|
||||
"""Raised when cairo returns an error status."""
|
||||
def __init__(self, message, status):
|
||||
super(CairoError, self).__init__(message)
|
||||
self.status = status
|
||||
|
||||
|
||||
Error = CairoError # pycairo compat
|
||||
|
||||
STATUS_TO_EXCEPTION = {
|
||||
constants.STATUS_NO_MEMORY: MemoryError,
|
||||
constants.STATUS_READ_ERROR: IOError,
|
||||
constants.STATUS_WRITE_ERROR: IOError,
|
||||
constants.STATUS_TEMP_FILE_ERROR: IOError,
|
||||
constants.STATUS_FILE_NOT_FOUND: FileNotFoundError,
|
||||
}
|
||||
|
||||
|
||||
def _check_status(status):
|
||||
"""Take a cairo status code and raise an exception if/as appropriate."""
|
||||
if status != constants.STATUS_SUCCESS:
|
||||
exception = STATUS_TO_EXCEPTION.get(status, CairoError)
|
||||
status_name = ffi.string(ffi.cast("cairo_status_t", status))
|
||||
message = 'cairo returned %s: %s' % (
|
||||
status_name, ffi.string(cairo.cairo_status_to_string(status)))
|
||||
raise exception(message, status)
|
||||
|
||||
|
||||
def cairo_version():
|
||||
"""Return the cairo version number as a single integer,
|
||||
such as 11208 for ``1.12.8``.
|
||||
Major, minor and micro versions are "worth" 10000, 100 and 1 respectively.
|
||||
|
||||
Can be useful as a guard for method not available in older cairo versions::
|
||||
|
||||
if cairo_version() >= 11000:
|
||||
surface.set_mime_data('image/jpeg', jpeg_bytes)
|
||||
|
||||
"""
|
||||
return cairo.cairo_version()
|
||||
|
||||
|
||||
def cairo_version_string():
|
||||
"""Return the cairo version number as a string, such as ``1.12.8``."""
|
||||
return ffi.string(cairo.cairo_version_string()).decode('ascii')
|
||||
|
||||
|
||||
def install_as_pycairo():
|
||||
"""Install cairocffi so that ``import cairo`` imports it.
|
||||
|
||||
cairoffi’s API is compatible with pycairo as much as possible.
|
||||
|
||||
"""
|
||||
sys.modules['cairo'] = sys.modules[__name__]
|
||||
|
||||
|
||||
# Implementation is in submodules, but public API is all here.
|
||||
|
||||
from .surfaces import ( # noqa isort:skip
|
||||
Surface, ImageSurface, PDFSurface, PSSurface, SVGSurface, RecordingSurface,
|
||||
Win32Surface, Win32PrintingSurface)
|
||||
try:
|
||||
from .xcb import XCBSurface # noqa isort:skip
|
||||
except ImportError:
|
||||
pass
|
||||
from .patterns import ( # noqa isort:skip
|
||||
Pattern, SolidPattern, SurfacePattern, Gradient, LinearGradient,
|
||||
RadialGradient)
|
||||
from .fonts import ( # noqa isort:skip
|
||||
FontFace, ToyFontFace, ScaledFont, FontOptions)
|
||||
from .context import Context # noqa isort:skip
|
||||
from .matrix import Matrix # noqa isort:skip
|
||||
|
||||
from .constants import * # noqa isort:skip
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
cairocffi.ffi
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Build the cffi bindings
|
||||
|
||||
:copyright: Copyright 2013-2019 by Simon Sapin
|
||||
:license: BSD, see LICENSE for details.
|
||||
|
||||
"""
|
||||
|
||||
from cffi import FFI
|
||||
|
||||
from . import constants
|
||||
|
||||
# Primary cffi definitions
|
||||
ffi = FFI()
|
||||
ffi.cdef(constants._CAIRO_HEADERS)
|
||||
|
||||
# include xcffib cffi definitions for cairo xcb support
|
||||
try:
|
||||
from xcffib.ffi import ffi as xcb_ffi
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
ffi.include(xcb_ffi)
|
||||
ffi.cdef(constants._CAIRO_XCB_HEADERS)
|
||||
|
||||
# gdk pixbuf cffi definitions
|
||||
ffi_pixbuf = FFI()
|
||||
ffi_pixbuf.include(ffi)
|
||||
ffi_pixbuf.cdef('''
|
||||
typedef unsigned long gsize;
|
||||
typedef unsigned int guint32;
|
||||
typedef unsigned int guint;
|
||||
typedef unsigned char guchar;
|
||||
typedef char gchar;
|
||||
typedef int gint;
|
||||
typedef gint gboolean;
|
||||
typedef guint32 GQuark;
|
||||
typedef void* gpointer;
|
||||
typedef ... GdkPixbufLoader;
|
||||
typedef ... GdkPixbufFormat;
|
||||
typedef ... GdkPixbuf;
|
||||
typedef struct {
|
||||
GQuark domain;
|
||||
gint code;
|
||||
gchar *message;
|
||||
} GError;
|
||||
typedef enum {
|
||||
GDK_COLORSPACE_RGB
|
||||
} GdkColorspace;
|
||||
|
||||
|
||||
GdkPixbufLoader * gdk_pixbuf_loader_new (void);
|
||||
GdkPixbufFormat * gdk_pixbuf_loader_get_format (GdkPixbufLoader *loader);
|
||||
GdkPixbuf * gdk_pixbuf_loader_get_pixbuf (GdkPixbufLoader *loader);
|
||||
gboolean gdk_pixbuf_loader_write (
|
||||
GdkPixbufLoader *loader, const guchar *buf, gsize count,
|
||||
GError **error);
|
||||
void gdk_pixbuf_loader_set_size (
|
||||
GdkPixbufLoader *loader, int width, int height);
|
||||
gboolean gdk_pixbuf_loader_close (
|
||||
GdkPixbufLoader *loader, GError **error);
|
||||
|
||||
gchar * gdk_pixbuf_format_get_name (GdkPixbufFormat *format);
|
||||
|
||||
GdkColorspace gdk_pixbuf_get_colorspace (const GdkPixbuf *pixbuf);
|
||||
int gdk_pixbuf_get_n_channels (const GdkPixbuf *pixbuf);
|
||||
gboolean gdk_pixbuf_get_has_alpha (const GdkPixbuf *pixbuf);
|
||||
int gdk_pixbuf_get_bits_per_sample (const GdkPixbuf *pixbuf);
|
||||
int gdk_pixbuf_get_width (const GdkPixbuf *pixbuf);
|
||||
int gdk_pixbuf_get_height (const GdkPixbuf *pixbuf);
|
||||
int gdk_pixbuf_get_rowstride (const GdkPixbuf *pixbuf);
|
||||
guchar * gdk_pixbuf_get_pixels (const GdkPixbuf *pixbuf);
|
||||
gsize gdk_pixbuf_get_byte_length (const GdkPixbuf *pixbuf);
|
||||
gboolean gdk_pixbuf_save_to_buffer (
|
||||
GdkPixbuf *pixbuf, gchar **buffer, gsize *buffer_size,
|
||||
const char *type, GError **error, ...);
|
||||
|
||||
void gdk_cairo_set_source_pixbuf (
|
||||
cairo_t *cr, const GdkPixbuf *pixbuf,
|
||||
double pixbuf_x, double pixbuf_y);
|
||||
|
||||
|
||||
void g_object_ref (gpointer object);
|
||||
void g_object_unref (gpointer object);
|
||||
void g_error_free (GError *error);
|
||||
void g_type_init (void);
|
||||
''')
|
||||
@@ -0,0 +1,536 @@
|
||||
"""
|
||||
cairocffi.fonts
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Bindings for font-related objects.
|
||||
|
||||
:copyright: Copyright 2013-2019 by Simon Sapin
|
||||
:license: BSD, see LICENSE for details.
|
||||
|
||||
"""
|
||||
|
||||
from . import _check_status, _keepref, cairo, constants, ffi
|
||||
from .matrix import Matrix
|
||||
|
||||
|
||||
def _encode_string(string):
|
||||
"""Return a byte string, encoding Unicode with UTF-8."""
|
||||
if not isinstance(string, bytes):
|
||||
string = string.encode('utf8')
|
||||
return ffi.new('char[]', string)
|
||||
|
||||
|
||||
class FontFace(object):
|
||||
"""The base class for all font face types.
|
||||
|
||||
Should not be instantiated directly, but see :doc:`cffi_api`.
|
||||
An instance may be returned for cairo font face types
|
||||
that are not (yet) defined in cairocffi.
|
||||
|
||||
"""
|
||||
def __init__(self, pointer):
|
||||
self._pointer = ffi.gc(
|
||||
pointer, _keepref(cairo, cairo.cairo_font_face_destroy))
|
||||
self._check_status()
|
||||
|
||||
def _check_status(self):
|
||||
_check_status(cairo.cairo_font_face_status(self._pointer))
|
||||
|
||||
@staticmethod
|
||||
def _from_pointer(pointer, incref):
|
||||
"""Wrap an existing ``cairo_font_face_t *`` cdata pointer.
|
||||
|
||||
:type incref: bool
|
||||
:param incref:
|
||||
Whether increase the :ref:`reference count <refcounting>` now.
|
||||
:return:
|
||||
A new instance of :class:`FontFace` or one of its sub-classes,
|
||||
depending on the face’s type.
|
||||
|
||||
"""
|
||||
if pointer == ffi.NULL:
|
||||
raise ValueError('Null pointer')
|
||||
if incref:
|
||||
cairo.cairo_font_face_reference(pointer)
|
||||
self = object.__new__(FONT_TYPE_TO_CLASS.get(
|
||||
cairo.cairo_font_face_get_type(pointer), FontFace))
|
||||
FontFace.__init__(self, pointer) # Skip the subclass’s __init__
|
||||
return self
|
||||
|
||||
|
||||
class ToyFontFace(FontFace):
|
||||
"""Creates a font face from a triplet of family, slant, and weight.
|
||||
These font faces are used in implementation of cairo’s "toy" font API.
|
||||
|
||||
If family is the zero-length string ``""``,
|
||||
the platform-specific default family is assumed.
|
||||
The default family then can be queried using :meth:`get_family`.
|
||||
|
||||
The :meth:`Context.select_font_face` method uses this to create font faces.
|
||||
See that method for limitations and other details of toy font faces.
|
||||
|
||||
:param family: a font family name, as an Unicode or UTF-8 string.
|
||||
:param slant: The :ref:`FONT_SLANT` string for the font face.
|
||||
:param weight: The :ref:`FONT_WEIGHT` string for the font face.
|
||||
|
||||
"""
|
||||
def __init__(self, family='', slant=constants.FONT_SLANT_NORMAL,
|
||||
weight=constants.FONT_WEIGHT_NORMAL):
|
||||
FontFace.__init__(self, cairo.cairo_toy_font_face_create(
|
||||
_encode_string(family), slant, weight))
|
||||
|
||||
def get_family(self):
|
||||
"""Return this font face’s family name."""
|
||||
return ffi.string(cairo.cairo_toy_font_face_get_family(
|
||||
self._pointer)).decode('utf8', 'replace')
|
||||
|
||||
def get_slant(self):
|
||||
"""Return this font face’s :ref:`FONT_SLANT` string."""
|
||||
return cairo.cairo_toy_font_face_get_slant(self._pointer)
|
||||
|
||||
def get_weight(self):
|
||||
"""Return this font face’s :ref:`FONT_WEIGHT` string."""
|
||||
return cairo.cairo_toy_font_face_get_weight(self._pointer)
|
||||
|
||||
|
||||
FONT_TYPE_TO_CLASS = {
|
||||
constants.FONT_TYPE_TOY: ToyFontFace,
|
||||
}
|
||||
|
||||
|
||||
class ScaledFont(object):
|
||||
"""Creates a :class:`ScaledFont` object from a font face and matrices
|
||||
that describe the size of the font
|
||||
and the environment in which it will be used.
|
||||
|
||||
:param font_face: A :class:`FontFace` object.
|
||||
:type font_matrix: Matrix
|
||||
:param font_matrix:
|
||||
Font space to user space transformation matrix for the font.
|
||||
In the simplest case of a N point font,
|
||||
this matrix is just a scale by N,
|
||||
but it can also be used to shear the font
|
||||
or stretch it unequally along the two axes.
|
||||
If omitted, a scale by 10 matrix is assumed (ie. a 10 point font size).
|
||||
See :class:`Context.set_font_matrix`.
|
||||
:type ctm: Matrix
|
||||
:param ctm:
|
||||
User to device transformation matrix with which the font will be used.
|
||||
If omitted, an identity matrix is assumed.
|
||||
:param options:
|
||||
The :class:`FontOptions` object to use
|
||||
when getting metrics for the font and rendering with it.
|
||||
If omitted, the default options are assumed.
|
||||
|
||||
"""
|
||||
def __init__(self, font_face, font_matrix=None, ctm=None, options=None):
|
||||
if font_matrix is None:
|
||||
font_matrix = Matrix()
|
||||
font_matrix.scale(10) # Default font size
|
||||
if ctm is None:
|
||||
ctm = Matrix()
|
||||
if options is None:
|
||||
options = FontOptions()
|
||||
self._init_pointer(cairo.cairo_scaled_font_create(
|
||||
font_face._pointer, font_matrix._pointer,
|
||||
ctm._pointer, options._pointer))
|
||||
|
||||
def _init_pointer(self, pointer):
|
||||
self._pointer = ffi.gc(
|
||||
pointer, _keepref(cairo, cairo.cairo_scaled_font_destroy))
|
||||
self._check_status()
|
||||
|
||||
def _check_status(self):
|
||||
_check_status(cairo.cairo_scaled_font_status(self._pointer))
|
||||
|
||||
@staticmethod
|
||||
def _from_pointer(pointer, incref):
|
||||
"""Wrap an existing ``cairo_scaled_font_t *`` cdata pointer.
|
||||
|
||||
:type incref: bool
|
||||
:param incref:
|
||||
Whether increase the :ref:`reference count <refcounting>` now.
|
||||
:return: A new :class:`ScaledFont` instance.
|
||||
|
||||
"""
|
||||
if pointer == ffi.NULL:
|
||||
raise ValueError('Null pointer')
|
||||
if incref:
|
||||
cairo.cairo_scaled_font_reference(pointer)
|
||||
self = object.__new__(ScaledFont)
|
||||
ScaledFont._init_pointer(self, pointer)
|
||||
return self
|
||||
|
||||
def get_font_face(self):
|
||||
"""Return the font face that this scaled font uses.
|
||||
|
||||
:returns:
|
||||
A new instance of :class:`FontFace` (or one of its sub-classes).
|
||||
Might wrap be the same font face passed to :class:`ScaledFont`,
|
||||
but this does not hold true for all possible cases.
|
||||
|
||||
"""
|
||||
return FontFace._from_pointer(
|
||||
cairo.cairo_scaled_font_get_font_face(self._pointer), incref=True)
|
||||
|
||||
def get_font_options(self):
|
||||
"""Copies the scaled font’s options.
|
||||
|
||||
:returns: A new :class:`FontOptions` object.
|
||||
|
||||
"""
|
||||
font_options = FontOptions()
|
||||
cairo.cairo_scaled_font_get_font_options(
|
||||
self._pointer, font_options._pointer)
|
||||
return font_options
|
||||
|
||||
def get_font_matrix(self):
|
||||
"""Copies the scaled font’s font matrix.
|
||||
|
||||
:returns: A new :class:`Matrix` object.
|
||||
|
||||
"""
|
||||
matrix = Matrix()
|
||||
cairo.cairo_scaled_font_get_font_matrix(self._pointer, matrix._pointer)
|
||||
self._check_status()
|
||||
return matrix
|
||||
|
||||
def get_ctm(self):
|
||||
"""Copies the scaled font’s font current transform matrix.
|
||||
|
||||
Note that the translation offsets ``(x0, y0)`` of the CTM
|
||||
are ignored by :class:`ScaledFont`.
|
||||
So, the matrix this method returns always has 0 as ``x0`` and ``y0``.
|
||||
|
||||
:returns: A new :class:`Matrix` object.
|
||||
|
||||
"""
|
||||
matrix = Matrix()
|
||||
cairo.cairo_scaled_font_get_ctm(self._pointer, matrix._pointer)
|
||||
self._check_status()
|
||||
return matrix
|
||||
|
||||
def get_scale_matrix(self):
|
||||
"""Copies the scaled font’s scaled matrix.
|
||||
|
||||
The scale matrix is product of the font matrix
|
||||
and the ctm associated with the scaled font,
|
||||
and hence is the matrix mapping from font space to device space.
|
||||
|
||||
:returns: A new :class:`Matrix` object.
|
||||
|
||||
"""
|
||||
matrix = Matrix()
|
||||
cairo.cairo_scaled_font_get_scale_matrix(
|
||||
self._pointer, matrix._pointer)
|
||||
self._check_status()
|
||||
return matrix
|
||||
|
||||
def extents(self):
|
||||
"""Return the scaled font’s extents.
|
||||
See :meth:`Context.font_extents`.
|
||||
|
||||
:returns:
|
||||
A ``(ascent, descent, height, max_x_advance, max_y_advance)``
|
||||
tuple of floats.
|
||||
|
||||
"""
|
||||
extents = ffi.new('cairo_font_extents_t *')
|
||||
cairo.cairo_scaled_font_extents(self._pointer, extents)
|
||||
self._check_status()
|
||||
return (
|
||||
extents.ascent, extents.descent, extents.height,
|
||||
extents.max_x_advance, extents.max_y_advance)
|
||||
|
||||
def text_extents(self, text):
|
||||
"""Returns the extents for a string of text.
|
||||
|
||||
The extents describe a user-space rectangle
|
||||
that encloses the "inked" portion of the text,
|
||||
(as it would be drawn by :meth:`Context.show_text`).
|
||||
Additionally, the ``x_advance`` and ``y_advance`` values
|
||||
indicate the amount by which the current point would be advanced
|
||||
by :meth:`Context.show_text`.
|
||||
|
||||
:param text: The text to measure, as an Unicode or UTF-8 string.
|
||||
:returns:
|
||||
A ``(x_bearing, y_bearing, width, height, x_advance, y_advance)``
|
||||
tuple of floats.
|
||||
See :meth:`Context.text_extents` for details.
|
||||
|
||||
"""
|
||||
extents = ffi.new('cairo_text_extents_t *')
|
||||
cairo.cairo_scaled_font_text_extents(
|
||||
self._pointer, _encode_string(text), extents)
|
||||
self._check_status()
|
||||
return (
|
||||
extents.x_bearing, extents.y_bearing,
|
||||
extents.width, extents.height,
|
||||
extents.x_advance, extents.y_advance)
|
||||
|
||||
def glyph_extents(self, glyphs):
|
||||
"""Returns the extents for a list of glyphs.
|
||||
|
||||
The extents describe a user-space rectangle
|
||||
that encloses the "inked" portion of the glyphs,
|
||||
(as it would be drawn by :meth:`Context.show_glyphs`).
|
||||
Additionally, the ``x_advance`` and ``y_advance`` values
|
||||
indicate the amount by which the current point would be advanced
|
||||
by :meth:`Context.show_glyphs`.
|
||||
|
||||
:param glyphs:
|
||||
A list of glyphs, as returned by :meth:`text_to_glyphs`.
|
||||
Each glyph is a ``(glyph_id, x, y)`` tuple
|
||||
of an integer and two floats.
|
||||
:returns:
|
||||
A ``(x_bearing, y_bearing, width, height, x_advance, y_advance)``
|
||||
tuple of floats.
|
||||
See :meth:`Context.text_extents` for details.
|
||||
|
||||
"""
|
||||
glyphs = ffi.new('cairo_glyph_t[]', glyphs)
|
||||
extents = ffi.new('cairo_text_extents_t *')
|
||||
cairo.cairo_scaled_font_glyph_extents(
|
||||
self._pointer, glyphs, len(glyphs), extents)
|
||||
self._check_status()
|
||||
return (
|
||||
extents.x_bearing, extents.y_bearing,
|
||||
extents.width, extents.height,
|
||||
extents.x_advance, extents.y_advance)
|
||||
|
||||
def text_to_glyphs(self, x, y, text, with_clusters):
|
||||
"""Converts a string of text to a list of glyphs,
|
||||
optionally with cluster mapping,
|
||||
that can be used to render later using this scaled font.
|
||||
|
||||
The output values can be readily passed to
|
||||
:meth:`Context.show_text_glyphs`, :meth:`Context.show_glyphs`
|
||||
or related methods,
|
||||
assuming that the exact same :class:`ScaledFont`
|
||||
is used for the operation.
|
||||
|
||||
:type x: float
|
||||
:type y: float
|
||||
:type with_clusters: bool
|
||||
:param x: X position to place first glyph.
|
||||
:param y: Y position to place first glyph.
|
||||
:param text: The text to convert, as an Unicode or UTF-8 string.
|
||||
:param with_clusters: Whether to compute the cluster mapping.
|
||||
:returns:
|
||||
A ``(glyphs, clusters, clusters_flags)`` tuple
|
||||
if ``with_clusters`` is true, otherwise just ``glyphs``.
|
||||
See :meth:`Context.show_text_glyphs` for the data structure.
|
||||
|
||||
.. note::
|
||||
|
||||
This method is part of
|
||||
what the cairo designers call the "toy" text API.
|
||||
It is convenient for short demos and simple programs,
|
||||
but it is not expected to be adequate
|
||||
for serious text-using applications.
|
||||
See :ref:`fonts` for details
|
||||
and :meth:`Context.show_glyphs`
|
||||
for the "real" text display API in cairo.
|
||||
|
||||
"""
|
||||
glyphs = ffi.new('cairo_glyph_t **', ffi.NULL)
|
||||
num_glyphs = ffi.new('int *')
|
||||
if with_clusters:
|
||||
clusters = ffi.new('cairo_text_cluster_t **', ffi.NULL)
|
||||
num_clusters = ffi.new('int *')
|
||||
cluster_flags = ffi.new('cairo_text_cluster_flags_t *')
|
||||
else:
|
||||
clusters = ffi.NULL
|
||||
num_clusters = ffi.NULL
|
||||
cluster_flags = ffi.NULL
|
||||
# TODO: Pass len_utf8 explicitly to support NULL bytes?
|
||||
status = cairo.cairo_scaled_font_text_to_glyphs(
|
||||
self._pointer, x, y, _encode_string(text), -1,
|
||||
glyphs, num_glyphs, clusters, num_clusters, cluster_flags)
|
||||
glyphs = ffi.gc(glyphs[0], _keepref(cairo, cairo.cairo_glyph_free))
|
||||
if with_clusters:
|
||||
clusters = ffi.gc(
|
||||
clusters[0], _keepref(cairo, cairo.cairo_text_cluster_free))
|
||||
_check_status(status)
|
||||
glyphs = [
|
||||
(glyph.index, glyph.x, glyph.y)
|
||||
for i in range(num_glyphs[0])
|
||||
for glyph in [glyphs[i]]]
|
||||
if with_clusters:
|
||||
clusters = [
|
||||
(cluster.num_bytes, cluster.num_glyphs)
|
||||
for i in range(num_clusters[0])
|
||||
for cluster in [clusters[i]]]
|
||||
return glyphs, clusters, cluster_flags[0]
|
||||
else:
|
||||
return glyphs
|
||||
|
||||
|
||||
class FontOptions(object):
|
||||
"""An opaque object holding all options that are used when rendering fonts.
|
||||
|
||||
Individual features of a :class:`FontOptions`
|
||||
can be set or accessed using method
|
||||
named ``set_FEATURE_NAME`` and ``get_FEATURE_NAME``,
|
||||
like :meth:`set_antialias` and :meth:`get_antialias`.
|
||||
|
||||
New features may be added to :class:`FontOptions` in the future.
|
||||
For this reason, ``==``, :meth:`copy`, :meth:`merge`, and :func:`hash`
|
||||
should be used to check for equality copy,, merge,
|
||||
or compute a hash value of :class:`FontOptions` objects.
|
||||
|
||||
:param values:
|
||||
Call the corresponding ``set_FEATURE_NAME`` methods
|
||||
after creating a new :class:`FontOptions`::
|
||||
|
||||
options = FontOptions()
|
||||
options.set_antialias(cairocffi.ANTIALIAS_BEST)
|
||||
assert FontOptions(antialias=cairocffi.ANTIALIAS_BEST) == options
|
||||
|
||||
"""
|
||||
def __init__(self, **values):
|
||||
self._init_pointer(cairo.cairo_font_options_create())
|
||||
for name, value in values.items():
|
||||
getattr(self, 'set_' + name)(value)
|
||||
|
||||
def _init_pointer(self, pointer):
|
||||
self._pointer = ffi.gc(
|
||||
pointer, _keepref(cairo, cairo.cairo_font_options_destroy))
|
||||
self._check_status()
|
||||
|
||||
def _check_status(self):
|
||||
_check_status(cairo.cairo_font_options_status(self._pointer))
|
||||
|
||||
def copy(self):
|
||||
"""Return a new :class:`FontOptions` with the same values."""
|
||||
cls = type(self)
|
||||
other = object.__new__(cls)
|
||||
cls._init_pointer(other, cairo.cairo_font_options_copy(self._pointer))
|
||||
return other
|
||||
|
||||
def merge(self, other):
|
||||
"""Merges non-default options from ``other``,
|
||||
replacing existing values.
|
||||
This operation can be thought of as somewhat similar
|
||||
to compositing other onto options
|
||||
with the operation of :obj:`OVER <OPERATOR_OVER>`.
|
||||
|
||||
"""
|
||||
cairo.cairo_font_options_merge(self._pointer, other._pointer)
|
||||
_check_status(cairo.cairo_font_options_status(self._pointer))
|
||||
|
||||
def __hash__(self):
|
||||
return cairo.cairo_font_options_hash(self._pointer)
|
||||
|
||||
def __eq__(self, other):
|
||||
return cairo.cairo_font_options_equal(self._pointer, other._pointer)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
equal = __eq__
|
||||
hash = __hash__
|
||||
|
||||
def set_antialias(self, antialias):
|
||||
"""Changes the :ref:`ANTIALIAS` for the font options object.
|
||||
This specifies the type of antialiasing to do when rendering text.
|
||||
|
||||
"""
|
||||
cairo.cairo_font_options_set_antialias(self._pointer, antialias)
|
||||
self._check_status()
|
||||
|
||||
def get_antialias(self):
|
||||
"""Return the :ref:`ANTIALIAS` string for the font options object."""
|
||||
return cairo.cairo_font_options_get_antialias(self._pointer)
|
||||
|
||||
def set_subpixel_order(self, subpixel_order):
|
||||
"""Changes the :ref:`SUBPIXEL_ORDER` for the font options object.
|
||||
The subpixel order specifies the order of color elements
|
||||
within each pixel on the display device
|
||||
when rendering with an antialiasing mode of
|
||||
:obj:`SUBPIXEL <ANTIALIAS_SUBPIXEL>`.
|
||||
|
||||
"""
|
||||
cairo.cairo_font_options_set_subpixel_order(
|
||||
self._pointer, subpixel_order)
|
||||
self._check_status()
|
||||
|
||||
def get_subpixel_order(self):
|
||||
"""Return the :ref:`SUBPIXEL_ORDER` string
|
||||
for the font options object.
|
||||
|
||||
"""
|
||||
return cairo.cairo_font_options_get_subpixel_order(self._pointer)
|
||||
|
||||
def set_hint_style(self, hint_style):
|
||||
"""Changes the :ref:`HINT_STYLE` for the font options object.
|
||||
This controls whether to fit font outlines to the pixel grid,
|
||||
and if so, whether to optimize for fidelity or contrast.
|
||||
|
||||
"""
|
||||
cairo.cairo_font_options_set_hint_style(self._pointer, hint_style)
|
||||
self._check_status()
|
||||
|
||||
def get_hint_style(self):
|
||||
"""Return the :ref:`HINT_STYLE` string for the font options object."""
|
||||
return cairo.cairo_font_options_get_hint_style(self._pointer)
|
||||
|
||||
def set_hint_metrics(self, hint_metrics):
|
||||
"""Changes the :ref:`HINT_METRICS` for the font options object.
|
||||
This controls whether metrics are quantized
|
||||
to integer values in device units.
|
||||
|
||||
"""
|
||||
cairo.cairo_font_options_set_hint_metrics(self._pointer, hint_metrics)
|
||||
self._check_status()
|
||||
|
||||
def get_hint_metrics(self):
|
||||
"""Return the :ref:`HINT_METRICS` string
|
||||
for the font options object.
|
||||
|
||||
"""
|
||||
return cairo.cairo_font_options_get_hint_metrics(self._pointer)
|
||||
|
||||
def set_variations(self, variations):
|
||||
"""Sets the OpenType font variations for the font options object.
|
||||
|
||||
Font variations are specified as a string with a format that is similar
|
||||
to the CSS font-variation-settings. The string contains a
|
||||
comma-separated list of axis assignments, which each assignment
|
||||
consists of a 4-character axis name and a value, separated by
|
||||
whitespace and optional equals sign.
|
||||
|
||||
:param variations: the new font variations, or ``None``.
|
||||
|
||||
*New in cairo 1.16.*
|
||||
|
||||
*New in cairocffi 0.9.*
|
||||
|
||||
"""
|
||||
if variations is None:
|
||||
variations = ffi.NULL
|
||||
else:
|
||||
variations = _encode_string(variations)
|
||||
cairo.cairo_font_options_set_variations(self._pointer, variations)
|
||||
self._check_status()
|
||||
|
||||
def get_variations(self):
|
||||
"""Gets the OpenType font variations for the font options object.
|
||||
|
||||
See :meth:`set_variations` for details about the
|
||||
string format.
|
||||
|
||||
:return: the font variations for the font options object. The
|
||||
returned string belongs to the ``options`` and must not be modified.
|
||||
It is valid until either the font options object is destroyed or the
|
||||
font variations in this object is modified with
|
||||
:meth:`set_variations`.
|
||||
|
||||
*New in cairo 1.16.*
|
||||
|
||||
*New in cairocffi 0.9.*
|
||||
|
||||
"""
|
||||
variations = cairo.cairo_font_options_get_variations(self._pointer)
|
||||
if variations != ffi.NULL:
|
||||
return ffi.string(variations).decode('utf8', 'replace')
|
||||
@@ -0,0 +1,250 @@
|
||||
"""
|
||||
cairocffi.matrix
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Transformation matrices.
|
||||
|
||||
:copyright: Copyright 2013-2019 by Simon Sapin
|
||||
:license: BSD, see LICENSE for details.
|
||||
|
||||
"""
|
||||
|
||||
from . import _check_status, cairo, ffi
|
||||
|
||||
|
||||
class Matrix(object):
|
||||
"""A 2D transformation matrix.
|
||||
|
||||
Matrices are used throughout cairo to convert between
|
||||
different coordinate spaces.
|
||||
A :class:`Matrix` holds an affine transformation,
|
||||
such as a scale, rotation, shear, or a combination of these.
|
||||
The transformation of a point (x,y) is given by::
|
||||
|
||||
x_new = xx * x + xy * y + x0
|
||||
y_new = yx * x + yy * y + y0
|
||||
|
||||
The current transformation matrix of a :class:`Context`,
|
||||
represented as a :class:`Matrix`,
|
||||
defines the transformation from user-space coordinates
|
||||
to device-space coordinates.
|
||||
See :meth:`Context.get_matrix` and :meth:`Context.set_matrix`.
|
||||
|
||||
The default values produce an identity matrix.
|
||||
|
||||
Matrices can be compared with ``m1 == m2`` and ``m2 != m2``
|
||||
as well as multiplied with ``m3 = m1 * m2``.
|
||||
|
||||
"""
|
||||
def __init__(self, xx=1, yx=0, xy=0, yy=1, x0=0, y0=0):
|
||||
self._pointer = ffi.new('cairo_matrix_t *')
|
||||
cairo.cairo_matrix_init(self._pointer, xx, yx, xy, yy, x0, y0)
|
||||
|
||||
@classmethod
|
||||
def init_rotate(cls, radians):
|
||||
"""Return a new :class:`Matrix` for a transformation
|
||||
that rotates by ``radians``.
|
||||
|
||||
:type radians: float
|
||||
:param radians:
|
||||
Angle of rotation, in radians.
|
||||
The direction of rotation is defined such that
|
||||
positive angles rotate in the direction
|
||||
from the positive X axis toward the positive Y axis.
|
||||
With the default axis orientation of cairo,
|
||||
positive angles rotate in a clockwise direction.
|
||||
|
||||
"""
|
||||
result = cls()
|
||||
cairo.cairo_matrix_init_rotate(result._pointer, radians)
|
||||
return result
|
||||
|
||||
def as_tuple(self):
|
||||
"""Return all of the matrix’s components.
|
||||
|
||||
:returns: A ``(xx, yx, xy, yy, x0, y0)`` tuple of floats.
|
||||
|
||||
"""
|
||||
ptr = self._pointer
|
||||
return (ptr.xx, ptr.yx, ptr.xy, ptr.yy, ptr.x0, ptr.y0)
|
||||
|
||||
def copy(self):
|
||||
"""Return a new copy of this matrix."""
|
||||
return type(self)(*self.as_tuple())
|
||||
|
||||
def __getitem__(self, index):
|
||||
return getattr(
|
||||
self._pointer, ('xx', 'yx', 'xy', 'yy', 'x0', 'y0')[index])
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.as_tuple())
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.as_tuple() == other.as_tuple()
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.as_tuple() != other.as_tuple()
|
||||
|
||||
def __repr__(self):
|
||||
class_ = type(self)
|
||||
return '%s(%g, %g, %g, %g, %g, %g)' % (
|
||||
(class_.__name__, *self.as_tuple()))
|
||||
|
||||
def multiply(self, other):
|
||||
"""Multiply with another matrix
|
||||
and return the result as a new :class:`Matrix` object.
|
||||
Same as ``self * other``.
|
||||
|
||||
"""
|
||||
res = Matrix()
|
||||
cairo.cairo_matrix_multiply(
|
||||
res._pointer, self._pointer, other._pointer)
|
||||
return res
|
||||
|
||||
__mul__ = multiply
|
||||
|
||||
def translate(self, tx, ty):
|
||||
"""Applies a translation by ``tx``, ``ty``
|
||||
to the transformation in this matrix.
|
||||
|
||||
The effect of the new transformation is to
|
||||
first translate the coordinates by ``tx`` and ``ty``,
|
||||
then apply the original transformation to the coordinates.
|
||||
|
||||
.. note::
|
||||
This changes the matrix in-place.
|
||||
|
||||
:param tx: Amount to translate in the X direction.
|
||||
:param ty: Amount to translate in the Y direction.
|
||||
:type tx: float
|
||||
:type ty: float
|
||||
|
||||
"""
|
||||
cairo.cairo_matrix_translate(self._pointer, tx, ty)
|
||||
|
||||
def scale(self, sx, sy=None):
|
||||
"""Applies scaling by ``sx``, ``sy``
|
||||
to the transformation in this matrix.
|
||||
|
||||
The effect of the new transformation is to
|
||||
first scale the coordinates by ``sx`` and ``sy``,
|
||||
then apply the original transformation to the coordinates.
|
||||
|
||||
If ``sy`` is omitted, it is the same as ``sx``
|
||||
so that scaling preserves aspect ratios.
|
||||
|
||||
.. note::
|
||||
This changes the matrix in-place.
|
||||
|
||||
:param sx: Scale factor in the X direction.
|
||||
:param sy: Scale factor in the Y direction.
|
||||
:type sx: float
|
||||
:type sy: float
|
||||
|
||||
"""
|
||||
if sy is None:
|
||||
sy = sx
|
||||
cairo.cairo_matrix_scale(self._pointer, sx, sy)
|
||||
|
||||
def rotate(self, radians):
|
||||
"""Applies a rotation by ``radians``
|
||||
to the transformation in this matrix.
|
||||
|
||||
The effect of the new transformation is to
|
||||
first rotate the coordinates by ``radians``,
|
||||
then apply the original transformation to the coordinates.
|
||||
|
||||
.. note::
|
||||
This changes the matrix in-place.
|
||||
|
||||
:type radians: float
|
||||
:param radians:
|
||||
Angle of rotation, in radians.
|
||||
The direction of rotation is defined such that positive angles
|
||||
rotate in the direction from the positive X axis
|
||||
toward the positive Y axis.
|
||||
With the default axis orientation of cairo,
|
||||
positive angles rotate in a clockwise direction.
|
||||
|
||||
"""
|
||||
cairo.cairo_matrix_rotate(self._pointer, radians)
|
||||
|
||||
def invert(self):
|
||||
"""Changes matrix to be the inverse of its original value.
|
||||
Not all transformation matrices have inverses;
|
||||
if the matrix collapses points together (it is degenerate),
|
||||
then it has no inverse and this function will fail.
|
||||
|
||||
.. note::
|
||||
This changes the matrix in-place.
|
||||
|
||||
:raises: :exc:`CairoError` on degenerate matrices.
|
||||
|
||||
"""
|
||||
_check_status(cairo.cairo_matrix_invert(self._pointer))
|
||||
|
||||
def inverted(self):
|
||||
"""Return the inverse of this matrix. See :meth:`invert`.
|
||||
|
||||
:raises: :exc:`CairoError` on degenerate matrices.
|
||||
:returns: A new :class:`Matrix` object.
|
||||
|
||||
"""
|
||||
matrix = self.copy()
|
||||
matrix.invert()
|
||||
return matrix
|
||||
|
||||
def transform_point(self, x, y):
|
||||
"""Transforms the point ``(x, y)`` by this matrix.
|
||||
|
||||
:param x: X position.
|
||||
:param y: Y position.
|
||||
:type x: float
|
||||
:type y: float
|
||||
:returns: A ``(new_x, new_y)`` tuple of floats.
|
||||
|
||||
"""
|
||||
xy = ffi.new('double[2]', [x, y])
|
||||
cairo.cairo_matrix_transform_point(self._pointer, xy + 0, xy + 1)
|
||||
return tuple(xy)
|
||||
|
||||
def transform_distance(self, dx, dy):
|
||||
"""Transforms the distance vector ``(dx, dy)`` by this matrix.
|
||||
This is similar to :meth:`transform_point`
|
||||
except that the translation components of the transformation
|
||||
are ignored.
|
||||
The calculation of the returned vector is as follows::
|
||||
|
||||
dx2 = dx1 * xx + dy1 * xy
|
||||
dy2 = dx1 * yx + dy1 * yy
|
||||
|
||||
Affine transformations are position invariant,
|
||||
so the same vector always transforms to the same vector.
|
||||
If ``(x1, y1)`` transforms to ``(x2, y2)``
|
||||
then ``(x1 + dx1, y1 + dy1)`` will transform
|
||||
to ``(x1 + dx2, y1 + dy2)`` for all values of ``x1`` and ``x2``.
|
||||
|
||||
:param dx: X component of a distance vector.
|
||||
:param dy: Y component of a distance vector.
|
||||
:type dx: float
|
||||
:type dy: float
|
||||
:returns: A ``(new_dx, new_dy)`` tuple of floats.
|
||||
|
||||
"""
|
||||
xy = ffi.new('double[2]', [dx, dy])
|
||||
cairo.cairo_matrix_transform_distance(self._pointer, xy + 0, xy + 1)
|
||||
return tuple(xy)
|
||||
|
||||
def _component_property(name): # noqa: N805
|
||||
return property(
|
||||
lambda self: getattr(self._pointer, name),
|
||||
lambda self, value: setattr(self._pointer, name, value),
|
||||
doc='Read-write attribute access to a single float component.')
|
||||
|
||||
xx = _component_property('xx')
|
||||
yx = _component_property('yx')
|
||||
xy = _component_property('xy')
|
||||
yy = _component_property('yy')
|
||||
x0 = _component_property('x0')
|
||||
y0 = _component_property('y0')
|
||||
del _component_property
|
||||
@@ -0,0 +1,371 @@
|
||||
"""
|
||||
cairocffi.patterns
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Bindings for the various types of pattern objects.
|
||||
|
||||
:copyright: Copyright 2013-2019 by Simon Sapin
|
||||
:license: BSD, see LICENSE for details.
|
||||
|
||||
"""
|
||||
|
||||
from . import _check_status, _keepref, cairo, constants, ffi
|
||||
from .matrix import Matrix
|
||||
from .surfaces import Surface
|
||||
|
||||
|
||||
class Pattern(object):
|
||||
"""The base class for all pattern types.
|
||||
|
||||
Should not be instantiated directly, but see :doc:`cffi_api`.
|
||||
An instance may be returned for cairo pattern types
|
||||
that are not (yet) defined in cairocffi.
|
||||
|
||||
A :class:`Pattern` represents a source when drawing onto a surface.
|
||||
There are different sub-classes of :class:`Pattern`,
|
||||
for different types of sources;
|
||||
for example, :class:`SolidPattern` is a pattern for a solid color.
|
||||
|
||||
Other than instantiating the various :class:`Pattern` sub-classes,
|
||||
some of the pattern types can be implicitly created
|
||||
using various :class:`Context`; for example :meth:`Context.set_source_rgb`.
|
||||
|
||||
"""
|
||||
def __init__(self, pointer):
|
||||
self._pointer = ffi.gc(
|
||||
pointer, _keepref(cairo, cairo.cairo_pattern_destroy))
|
||||
self._check_status()
|
||||
|
||||
def _check_status(self):
|
||||
_check_status(cairo.cairo_pattern_status(self._pointer))
|
||||
|
||||
@staticmethod
|
||||
def _from_pointer(pointer, incref):
|
||||
"""Wrap an existing ``cairo_pattern_t *`` cdata pointer.
|
||||
|
||||
:type incref: bool
|
||||
:param incref:
|
||||
Whether increase the :ref:`reference count <refcounting>` now.
|
||||
:return:
|
||||
A new instance of :class:`Pattern` or one of its sub-classes,
|
||||
depending on the pattern’s type.
|
||||
|
||||
"""
|
||||
if pointer == ffi.NULL:
|
||||
raise ValueError('Null pointer')
|
||||
if incref:
|
||||
cairo.cairo_pattern_reference(pointer)
|
||||
self = object.__new__(PATTERN_TYPE_TO_CLASS.get(
|
||||
cairo.cairo_pattern_get_type(pointer), Pattern))
|
||||
Pattern.__init__(self, pointer) # Skip the subclass’s __init__
|
||||
return self
|
||||
|
||||
def set_extend(self, extend):
|
||||
"""
|
||||
Sets the mode to be used for drawing outside the area of this pattern.
|
||||
See :ref:`EXTEND` for details on the semantics of each extend strategy.
|
||||
|
||||
The default extend mode is
|
||||
:obj:`NONE <EXTEND_NONE>` for :class:`SurfacePattern`
|
||||
and :obj:`PAD <EXTEND_PAD>` for :class:`Gradient` patterns.
|
||||
|
||||
"""
|
||||
cairo.cairo_pattern_set_extend(self._pointer, extend)
|
||||
self._check_status()
|
||||
|
||||
def get_extend(self):
|
||||
"""Gets the current extend mode for this pattern.
|
||||
|
||||
:returns: A :ref:`EXTEND` string.
|
||||
|
||||
"""
|
||||
return cairo.cairo_pattern_get_extend(self._pointer)
|
||||
|
||||
# pycairo only has filters on SurfacePattern,
|
||||
# but cairo seems to accept it on any pattern.
|
||||
def set_filter(self, filter):
|
||||
"""Sets the filter to be used for resizing when using this pattern.
|
||||
See :ref:`FILTER` for details on each filter.
|
||||
|
||||
Note that you might want to control filtering
|
||||
even when you do not have an explicit :class:`Pattern`,
|
||||
(for example when using :meth:`Context.set_source_surface`).
|
||||
In these cases, it is convenient to use :meth:`Context.get_source`
|
||||
to get access to the pattern that cairo creates implicitly.
|
||||
|
||||
For example::
|
||||
|
||||
context.get_source().set_filter(cairocffi.FILTER_NEAREST)
|
||||
|
||||
"""
|
||||
cairo.cairo_pattern_set_filter(self._pointer, filter)
|
||||
self._check_status()
|
||||
|
||||
def get_filter(self):
|
||||
"""Return the current filter string for this pattern.
|
||||
See :ref:`FILTER` for details on each filter.
|
||||
|
||||
"""
|
||||
return cairo.cairo_pattern_get_filter(self._pointer)
|
||||
|
||||
def set_matrix(self, matrix):
|
||||
"""Sets the pattern’s transformation matrix to ``matrix``.
|
||||
This matrix is a transformation from user space to pattern space.
|
||||
|
||||
When a pattern is first created
|
||||
it always has the identity matrix for its transformation matrix,
|
||||
which means that pattern space is initially identical to user space.
|
||||
|
||||
**Important:**
|
||||
Please note that the direction of this transformation matrix
|
||||
is from user space to pattern space.
|
||||
This means that if you imagine the flow
|
||||
from a pattern to user space (and on to device space),
|
||||
then coordinates in that flow will be transformed
|
||||
by the inverse of the pattern matrix.
|
||||
|
||||
For example, if you want to make a pattern appear twice as large
|
||||
as it does by default the correct code to use is::
|
||||
|
||||
pattern.set_matrix(Matrix(xx=0.5, yy=0.5))
|
||||
|
||||
Meanwhile, using values of 2 rather than 0.5 in the code above
|
||||
would cause the pattern to appear at half of its default size.
|
||||
|
||||
Also, please note the discussion of the user-space locking semantics
|
||||
of :meth:`Context.set_source`.
|
||||
|
||||
:param matrix: A :class:`Matrix` to be copied into the pattern.
|
||||
|
||||
"""
|
||||
cairo.cairo_pattern_set_matrix(self._pointer, matrix._pointer)
|
||||
self._check_status()
|
||||
|
||||
def get_matrix(self):
|
||||
"""Copies the pattern’s transformation matrix.
|
||||
|
||||
:retuns: A new :class:`Matrix` object.
|
||||
|
||||
"""
|
||||
matrix = Matrix()
|
||||
cairo.cairo_pattern_get_matrix(self._pointer, matrix._pointer)
|
||||
self._check_status()
|
||||
return matrix
|
||||
|
||||
|
||||
class SolidPattern(Pattern):
|
||||
"""Creates a new pattern corresponding to a solid color.
|
||||
The color and alpha components are in the range 0 to 1.
|
||||
If the values passed in are outside that range, they will be clamped.
|
||||
|
||||
:param red: Red component of the color.
|
||||
:param green: Green component of the color.
|
||||
:param blue: Blue component of the color.
|
||||
:param alpha:
|
||||
Alpha component of the color.
|
||||
1 (the default) is opaque, 0 fully transparent.
|
||||
:type red: float
|
||||
:type green: float
|
||||
:type blue: float
|
||||
:type alpha: float
|
||||
|
||||
"""
|
||||
def __init__(self, red, green, blue, alpha=1):
|
||||
Pattern.__init__(
|
||||
self, cairo.cairo_pattern_create_rgba(red, green, blue, alpha))
|
||||
|
||||
def get_rgba(self):
|
||||
"""Returns the solid pattern’s color.
|
||||
|
||||
:returns: a ``(red, green, blue, alpha)`` tuple of floats.
|
||||
|
||||
"""
|
||||
rgba = ffi.new('double[4]')
|
||||
_check_status(cairo.cairo_pattern_get_rgba(
|
||||
self._pointer, rgba + 0, rgba + 1, rgba + 2, rgba + 3))
|
||||
return tuple(rgba)
|
||||
|
||||
|
||||
class SurfacePattern(Pattern):
|
||||
"""Create a new pattern for the given surface.
|
||||
|
||||
:param surface: A :class:`Surface` object.
|
||||
|
||||
"""
|
||||
def __init__(self, surface):
|
||||
Pattern.__init__(
|
||||
self, cairo.cairo_pattern_create_for_surface(surface._pointer))
|
||||
|
||||
def get_surface(self):
|
||||
"""Return this :class:`SurfacePattern`’s surface.
|
||||
|
||||
:returns:
|
||||
An instance of :class:`Surface` or one of its sub-classes,
|
||||
a new Python object referencing the existing cairo surface.
|
||||
|
||||
"""
|
||||
surface_p = ffi.new('cairo_surface_t **')
|
||||
_check_status(cairo.cairo_pattern_get_surface(
|
||||
self._pointer, surface_p))
|
||||
return Surface._from_pointer(surface_p[0], incref=True)
|
||||
|
||||
|
||||
class Gradient(Pattern):
|
||||
"""
|
||||
The common parent of :class:`LinearGradient` and :class:`RadialGradient`.
|
||||
Should not be instantiated directly.
|
||||
|
||||
"""
|
||||
def add_color_stop_rgba(self, offset, red, green, blue, alpha=1):
|
||||
"""Adds a translucent color stop to a gradient pattern.
|
||||
|
||||
The offset specifies the location along the gradient's control vector.
|
||||
For example,
|
||||
a linear gradient's control vector is from (x0,y0) to (x1,y1)
|
||||
while a radial gradient's control vector is
|
||||
from any point on the start circle
|
||||
to the corresponding point on the end circle.
|
||||
|
||||
If two (or more) stops are specified with identical offset values,
|
||||
they will be sorted
|
||||
according to the order in which the stops are added
|
||||
(stops added earlier before stops added later).
|
||||
This can be useful for reliably making sharp color transitions
|
||||
instead of the typical blend.
|
||||
|
||||
The color components and offset are in the range 0 to 1.
|
||||
If the values passed in are outside that range, they will be clamped.
|
||||
|
||||
:param offset: Location along the gradient's control vector
|
||||
:param red: Red component of the color.
|
||||
:param green: Green component of the color.
|
||||
:param blue: Blue component of the color.
|
||||
:param alpha:
|
||||
Alpha component of the color.
|
||||
1 (the default) is opaque, 0 fully transparent.
|
||||
:type offset: float
|
||||
:type red: float
|
||||
:type green: float
|
||||
:type blue: float
|
||||
:type alpha: float
|
||||
|
||||
"""
|
||||
cairo.cairo_pattern_add_color_stop_rgba(
|
||||
self._pointer, offset, red, green, blue, alpha)
|
||||
self._check_status()
|
||||
|
||||
def add_color_stop_rgb(self, offset, red, green, blue):
|
||||
"""Same as :meth:`add_color_stop_rgba` with ``alpha=1``.
|
||||
Kept for compatibility with pycairo.
|
||||
|
||||
"""
|
||||
cairo.cairo_pattern_add_color_stop_rgb(
|
||||
self._pointer, offset, red, green, blue)
|
||||
self._check_status()
|
||||
|
||||
def get_color_stops(self):
|
||||
"""Return this gradient’s color stops so far.
|
||||
|
||||
:returns:
|
||||
A list of ``(offset, red, green, blue, alpha)`` tuples of floats.
|
||||
|
||||
"""
|
||||
count = ffi.new('int *')
|
||||
_check_status(cairo.cairo_pattern_get_color_stop_count(
|
||||
self._pointer, count))
|
||||
stops = []
|
||||
stop = ffi.new('double[5]')
|
||||
for i in range(count[0]):
|
||||
_check_status(cairo.cairo_pattern_get_color_stop_rgba(
|
||||
self._pointer, i,
|
||||
stop + 0, stop + 1, stop + 2, stop + 3, stop + 4))
|
||||
stops.append(tuple(stop))
|
||||
return stops
|
||||
|
||||
|
||||
class LinearGradient(Gradient):
|
||||
"""Create a new linear gradient
|
||||
along the line defined by (x0, y0) and (x1, y1).
|
||||
Before using the gradient pattern, a number of color stops
|
||||
should be defined using :meth:`~Gradient.add_color_stop_rgba`.
|
||||
|
||||
Note: The coordinates here are in pattern space.
|
||||
For a new pattern, pattern space is identical to user space,
|
||||
but the relationship between the spaces can be changed
|
||||
with :meth:`~Pattern.set_matrix`.
|
||||
|
||||
:param x0: X coordinate of the start point.
|
||||
:param y0: Y coordinate of the start point.
|
||||
:param x1: X coordinate of the end point.
|
||||
:param y1: Y coordinate of the end point.
|
||||
:type x0: float
|
||||
:type y0: float
|
||||
:type x1: float
|
||||
:type y1: float
|
||||
|
||||
"""
|
||||
def __init__(self, x0, y0, x1, y1):
|
||||
Pattern.__init__(
|
||||
self, cairo.cairo_pattern_create_linear(x0, y0, x1, y1))
|
||||
|
||||
def get_linear_points(self):
|
||||
"""Return this linear gradient’s endpoints.
|
||||
|
||||
:returns: A ``(x0, y0, x1, y1)`` tuple of floats.
|
||||
|
||||
"""
|
||||
points = ffi.new('double[4]')
|
||||
_check_status(cairo.cairo_pattern_get_linear_points(
|
||||
self._pointer, points + 0, points + 1, points + 2, points + 3))
|
||||
return tuple(points)
|
||||
|
||||
|
||||
class RadialGradient(Gradient):
|
||||
"""Creates a new radial gradient pattern between the two circles
|
||||
defined by (cx0, cy0, radius0) and (cx1, cy1, radius1).
|
||||
Before using the gradient pattern, a number of color stops
|
||||
should be defined using :meth:`~Gradient.add_color_stop_rgba`.
|
||||
|
||||
Note: The coordinates here are in pattern space.
|
||||
For a new pattern, pattern space is identical to user space,
|
||||
but the relationship between the spaces can be changed
|
||||
with :meth:`~Pattern.set_matrix`.
|
||||
|
||||
:param cx0: X coordinate of the start circle.
|
||||
:param cy0: Y coordinate of the start circle.
|
||||
:param radius0: Radius of the start circle.
|
||||
:param cx1: X coordinate of the end circle.
|
||||
:param cy1: Y coordinate of the end circle.
|
||||
:param radius1: Y coordinate of the end circle.
|
||||
:type cx0: float
|
||||
:type cy0: float
|
||||
:type radius0: float
|
||||
:type cx1: float
|
||||
:type cy1: float
|
||||
:type radius1: float
|
||||
|
||||
"""
|
||||
def __init__(self, cx0, cy0, radius0, cx1, cy1, radius1):
|
||||
Pattern.__init__(self, cairo.cairo_pattern_create_radial(
|
||||
cx0, cy0, radius0, cx1, cy1, radius1))
|
||||
|
||||
def get_radial_circles(self):
|
||||
"""Return this radial gradient’s endpoint circles,
|
||||
each specified as a center coordinate and a radius.
|
||||
|
||||
:returns: A ``(cx0, cy0, radius0, cx1, cy1, radius1)`` tuple of floats.
|
||||
|
||||
"""
|
||||
circles = ffi.new('double[6]')
|
||||
_check_status(cairo.cairo_pattern_get_radial_circles(
|
||||
self._pointer, circles + 0, circles + 1, circles + 2,
|
||||
circles + 3, circles + 4, circles + 5))
|
||||
return tuple(circles)
|
||||
|
||||
|
||||
PATTERN_TYPE_TO_CLASS = {
|
||||
constants.PATTERN_TYPE_SOLID: SolidPattern,
|
||||
constants.PATTERN_TYPE_SURFACE: SurfacePattern,
|
||||
constants.PATTERN_TYPE_LINEAR: LinearGradient,
|
||||
constants.PATTERN_TYPE_RADIAL: RadialGradient,
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
cairocffi.pixbuf
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Loading various image formats with GDK-PixBuf
|
||||
|
||||
:copyright: Copyright 2013-2019 by Simon Sapin
|
||||
:license: BSD, see LICENSE for details.
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
from array import array
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
|
||||
from . import Context, ImageSurface, constants, dlopen
|
||||
from .ffi import ffi_pixbuf as ffi
|
||||
|
||||
__all__ = ['decode_to_image_surface']
|
||||
|
||||
gdk_pixbuf = dlopen(
|
||||
ffi, ('gdk_pixbuf-2.0', 'libgdk_pixbuf-2.0-0'),
|
||||
('libgdk_pixbuf-2.0.so.0', 'libgdk_pixbuf-2.0.0.dylib',
|
||||
'libgdk_pixbuf-2.0-0.dll'))
|
||||
gobject = dlopen(
|
||||
ffi, ('gobject-2.0', 'libgobject-2.0-0'),
|
||||
('libgobject-2.0.so.0', 'libgobject-2.0.dylib', 'libgobject-2.0-0.dll'))
|
||||
glib = dlopen(
|
||||
ffi, ('glib-2.0', 'libglib-2.0-0'),
|
||||
('libglib-2.0.so.0', 'libglib-2.0.dylib', 'libglib-2.0-0.dll'))
|
||||
try:
|
||||
gdk = dlopen(
|
||||
ffi, ('gdk-3', 'libgdk-3-0'),
|
||||
('libgdk-3.so.0', 'libgdk-3.0.dylib', 'libgdk-3-0.dll'))
|
||||
except OSError:
|
||||
gdk = None
|
||||
|
||||
gobject.g_type_init()
|
||||
|
||||
|
||||
class ImageLoadingError(ValueError):
|
||||
"""PixBuf returned an error when loading an image.
|
||||
|
||||
The image data is probably corrupted.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def handle_g_error(error, return_value):
|
||||
"""Convert a ``GError**`` to a Python :exception:`ImageLoadingError`,
|
||||
and raise it.
|
||||
|
||||
"""
|
||||
error = error[0]
|
||||
assert bool(return_value) == (error == ffi.NULL)
|
||||
if error != ffi.NULL:
|
||||
if error.message != ffi.NULL:
|
||||
message = ('Pixbuf error: ' +
|
||||
ffi.string(error.message).decode('utf8', 'replace'))
|
||||
else: # pragma: no cover
|
||||
message = 'Pixbuf error'
|
||||
glib.g_error_free(error)
|
||||
raise ImageLoadingError(message)
|
||||
|
||||
|
||||
class Pixbuf(object):
|
||||
"""Wrap a ``GdkPixbuf`` pointer and simulate methods."""
|
||||
def __init__(self, pointer):
|
||||
gobject.g_object_ref(pointer)
|
||||
self._pointer = ffi.gc(pointer, gobject.g_object_unref)
|
||||
|
||||
def __getattr__(self, name):
|
||||
function = getattr(gdk_pixbuf, 'gdk_pixbuf_' + name)
|
||||
return partial(function, self._pointer)
|
||||
|
||||
|
||||
def decode_to_pixbuf(image_data, width=None, height=None):
|
||||
"""Decode an image from memory with GDK-PixBuf.
|
||||
The file format is detected automatically.
|
||||
|
||||
:param image_data: A byte string
|
||||
:param width: Integer width in pixels or None
|
||||
:param height: Integer height in pixels or None
|
||||
:returns:
|
||||
A tuple of a new :class:`PixBuf` object
|
||||
and the name of the detected image format.
|
||||
:raises:
|
||||
:exc:`ImageLoadingError` if the image data is invalid
|
||||
or in an unsupported format.
|
||||
|
||||
"""
|
||||
loader = ffi.gc(
|
||||
gdk_pixbuf.gdk_pixbuf_loader_new(), gobject.g_object_unref)
|
||||
error = ffi.new('GError **')
|
||||
if width and height:
|
||||
gdk_pixbuf.gdk_pixbuf_loader_set_size(loader, width, height)
|
||||
handle_g_error(error, gdk_pixbuf.gdk_pixbuf_loader_write(
|
||||
loader, image_data, len(image_data), error))
|
||||
handle_g_error(error, gdk_pixbuf.gdk_pixbuf_loader_close(loader, error))
|
||||
|
||||
format_ = gdk_pixbuf.gdk_pixbuf_loader_get_format(loader)
|
||||
format_name = (
|
||||
ffi.string(gdk_pixbuf.gdk_pixbuf_format_get_name(format_))
|
||||
.decode('ascii')
|
||||
if format_ != ffi.NULL else None)
|
||||
|
||||
pixbuf = gdk_pixbuf.gdk_pixbuf_loader_get_pixbuf(loader)
|
||||
if pixbuf == ffi.NULL: # pragma: no cover
|
||||
raise ImageLoadingError('Not enough image data (got a NULL pixbuf.)')
|
||||
return Pixbuf(pixbuf), format_name
|
||||
|
||||
|
||||
def decode_to_image_surface(image_data, width=None, height=None):
|
||||
"""Decode an image from memory into a cairo surface.
|
||||
The file format is detected automatically.
|
||||
|
||||
:param image_data: A byte string
|
||||
:param width: Integer width in pixels or None
|
||||
:param height: Integer height in pixels or None
|
||||
:returns:
|
||||
A tuple of a new :class:`~cairocffi.ImageSurface` object
|
||||
and the name of the detected image format.
|
||||
:raises:
|
||||
:exc:`ImageLoadingError` if the image data is invalid
|
||||
or in an unsupported format.
|
||||
|
||||
"""
|
||||
pixbuf, format_name = decode_to_pixbuf(image_data, width, height)
|
||||
surface = (
|
||||
pixbuf_to_cairo_gdk(pixbuf) if gdk is not None
|
||||
else pixbuf_to_cairo_slices(pixbuf) if not pixbuf.get_has_alpha()
|
||||
else pixbuf_to_cairo_png(pixbuf))
|
||||
return surface, format_name
|
||||
|
||||
|
||||
def pixbuf_to_cairo_gdk(pixbuf):
|
||||
"""Convert from PixBuf to ImageSurface, using GDK.
|
||||
|
||||
This method is fastest but GDK is not always available.
|
||||
|
||||
"""
|
||||
dummy_context = Context(ImageSurface(constants.FORMAT_ARGB32, 1, 1))
|
||||
gdk.gdk_cairo_set_source_pixbuf(
|
||||
dummy_context._pointer, pixbuf._pointer, 0, 0)
|
||||
return dummy_context.get_source().get_surface()
|
||||
|
||||
|
||||
def pixbuf_to_cairo_slices(pixbuf):
|
||||
"""Convert from PixBuf to ImageSurface, using slice-based byte swapping.
|
||||
|
||||
This method is 2~5x slower than GDK but does not support an alpha channel.
|
||||
(cairo uses pre-multiplied alpha, but not Pixbuf.)
|
||||
|
||||
"""
|
||||
assert pixbuf.get_colorspace() == gdk_pixbuf.GDK_COLORSPACE_RGB
|
||||
assert pixbuf.get_n_channels() == 3
|
||||
assert pixbuf.get_bits_per_sample() == 8
|
||||
width = pixbuf.get_width()
|
||||
height = pixbuf.get_height()
|
||||
rowstride = pixbuf.get_rowstride()
|
||||
pixels = ffi.buffer(pixbuf.get_pixels(), pixbuf.get_byte_length())
|
||||
# TODO: remove this when cffi buffers support slicing with a stride.
|
||||
pixels = pixels[:]
|
||||
|
||||
# Convert GdkPixbuf’s big-endian RGBA to cairo’s native-endian ARGB
|
||||
cairo_stride = ImageSurface.format_stride_for_width(
|
||||
constants.FORMAT_RGB24, width)
|
||||
data = bytearray(cairo_stride * height)
|
||||
big_endian = sys.byteorder == 'big'
|
||||
pixbuf_row_length = width * 3 # stride == row_length + padding
|
||||
cairo_row_length = width * 4 # stride == row_length + padding
|
||||
alpha = b'\xff' * width # opaque
|
||||
for y in range(height):
|
||||
offset = rowstride * y
|
||||
end = offset + pixbuf_row_length
|
||||
red = pixels[offset:end:3]
|
||||
green = pixels[offset + 1:end:3]
|
||||
blue = pixels[offset + 2:end:3]
|
||||
|
||||
offset = cairo_stride * y
|
||||
end = offset + cairo_row_length
|
||||
if big_endian: # pragma: no cover
|
||||
data[offset:end:4] = alpha
|
||||
data[offset + 1:end:4] = red
|
||||
data[offset + 2:end:4] = green
|
||||
data[offset + 3:end:4] = blue
|
||||
else:
|
||||
data[offset + 3:end:4] = alpha
|
||||
data[offset + 2:end:4] = red
|
||||
data[offset + 1:end:4] = green
|
||||
data[offset:end:4] = blue
|
||||
|
||||
data = array('B', data)
|
||||
return ImageSurface(constants.FORMAT_RGB24,
|
||||
width, height, data, cairo_stride)
|
||||
|
||||
|
||||
def pixbuf_to_cairo_png(pixbuf):
|
||||
"""Convert from PixBuf to ImageSurface, by going through the PNG format.
|
||||
|
||||
This method is 10~30x slower than GDK but always works.
|
||||
|
||||
"""
|
||||
buffer_pointer = ffi.new('gchar **')
|
||||
buffer_size = ffi.new('gsize *')
|
||||
error = ffi.new('GError **')
|
||||
handle_g_error(error, pixbuf.save_to_buffer(
|
||||
buffer_pointer, buffer_size, ffi.new('char[]', b'png'), error,
|
||||
ffi.new('char[]', b'compression'), ffi.new('char[]', b'0'),
|
||||
ffi.NULL))
|
||||
png_bytes = ffi.buffer(buffer_pointer[0], buffer_size[0])
|
||||
return ImageSurface.create_from_png(BytesIO(png_bytes))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
import math
|
||||
|
||||
import numpy
|
||||
|
||||
import cairocffi as cairo
|
||||
|
||||
|
||||
def test_numpy():
|
||||
data = numpy.zeros((200, 200, 4), dtype=numpy.uint8)
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 200, 200, data=data)
|
||||
cr = cairo.Context(surface)
|
||||
cr.set_source_rgb(1.0, 1.0, 1.0)
|
||||
cr.paint()
|
||||
cr.arc(100, 100, 80, 0, 2*math.pi)
|
||||
cr.set_line_width(3)
|
||||
cr.set_source_rgb(1.0, 0.0, 0.0)
|
||||
cr.stroke()
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
cairocffi.test_pixbuf
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Test suite for cairocffi.pixbuf.
|
||||
|
||||
:copyright: Copyright 2013-2019 by Simon Sapin
|
||||
:license: BSD, see LICENSE for details.
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
import pytest
|
||||
|
||||
from . import constants, pixbuf
|
||||
|
||||
PNG_BYTES = base64.b64decode(
|
||||
b'iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAE0lEQV'
|
||||
b'QI12NkaPjfwAAFTAxIAAAuNwIDqJbDRgAAAABJRU5ErkJggg==')
|
||||
|
||||
JPEG_BYTES = zlib.decompress(base64.b64decode(
|
||||
b'eJylzb0JgFAMBOA704hYvIC9oygIou7nPFq4g3+Nm0RT+iy9VPkIF9vsQhjavgVJdM/ATjS'
|
||||
b'+/YqX/O2gzdAUCUSoSJSitAUFiHdS1xArXBlr5qrf2wO58HkiigrlWK+T7TezChqU'))
|
||||
|
||||
|
||||
def test_api():
|
||||
with pytest.raises(pixbuf.ImageLoadingError):
|
||||
pixbuf.decode_to_image_surface(b'')
|
||||
with pytest.raises(pixbuf.ImageLoadingError):
|
||||
pixbuf.decode_to_image_surface(b'Not a valid image.')
|
||||
with pytest.raises(pixbuf.ImageLoadingError):
|
||||
pixbuf.decode_to_image_surface(PNG_BYTES[:10])
|
||||
surface, format_name = pixbuf.decode_to_image_surface(PNG_BYTES)
|
||||
assert format_name == 'png'
|
||||
assert_decoded(surface)
|
||||
|
||||
|
||||
def test_gdk():
|
||||
if pixbuf.gdk is None:
|
||||
pytest.xfail()
|
||||
pixbuf_obj, format_name = pixbuf.decode_to_pixbuf(PNG_BYTES)
|
||||
assert format_name == 'png'
|
||||
assert_decoded(pixbuf.pixbuf_to_cairo_gdk(pixbuf_obj))
|
||||
|
||||
|
||||
def test_slices():
|
||||
pixbuf_obj, format_name = pixbuf.decode_to_pixbuf(PNG_BYTES)
|
||||
assert format_name == 'png'
|
||||
assert_decoded(pixbuf.pixbuf_to_cairo_png(pixbuf_obj))
|
||||
|
||||
|
||||
def test_size():
|
||||
pixbuf_obj, format_name = pixbuf.decode_to_pixbuf(PNG_BYTES, 10, 10)
|
||||
assert format_name == 'png'
|
||||
surface = pixbuf.pixbuf_to_cairo_png(pixbuf_obj)
|
||||
assert surface.get_width() == 10
|
||||
assert surface.get_height() == 10
|
||||
assert surface.get_format() == constants.FORMAT_ARGB32
|
||||
|
||||
|
||||
def test_png():
|
||||
pixbuf_obj, format_name = pixbuf.decode_to_pixbuf(JPEG_BYTES)
|
||||
assert format_name == 'jpeg'
|
||||
assert_decoded(pixbuf.pixbuf_to_cairo_slices(pixbuf_obj),
|
||||
constants.FORMAT_RGB24, b'\xff\x00\x80\xff')
|
||||
|
||||
|
||||
def assert_decoded(surface, format_=constants.FORMAT_ARGB32,
|
||||
rgba=b'\x80\x00\x40\x80'):
|
||||
assert surface.get_width() == 3
|
||||
assert surface.get_height() == 2
|
||||
assert surface.get_format() == format_
|
||||
if sys.byteorder == 'little': # pragma: no cover
|
||||
rgba = rgba[::-1]
|
||||
assert surface.get_data()[:] == rgba * 6
|
||||
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
cairocffi.test_xcb
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Test suite for cairocffi.xcb.
|
||||
|
||||
:copyright: Copyright 2014-2019 by Simon Sapin
|
||||
:license: BSD, see LICENSE for details.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
xcffib = pytest.importorskip('xcffib')
|
||||
|
||||
import xcffib.xproto # noqa isort:skip
|
||||
from xcffib.xproto import ConfigWindow, CW, EventMask, GC # noqa isort:skip
|
||||
|
||||
from . import Context, XCBSurface, cairo_version # noqa isort:skip
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def xcb_conn():
|
||||
"""
|
||||
Fixture that will setup and take down a xcffib.Connection object running on
|
||||
a display spawned by xvfb
|
||||
"""
|
||||
display = os.environ.get('DISPLAY')
|
||||
if display is None: # pragma: no cover
|
||||
pytest.skip('DISPLAY environment variable not set')
|
||||
|
||||
conn = xcffib.connect(display)
|
||||
yield conn
|
||||
conn.disconnect()
|
||||
|
||||
|
||||
def find_root_visual(conn):
|
||||
"""Find the xcffib.xproto.VISUALTYPE corresponding to the root visual"""
|
||||
default_screen = conn.setup.roots[conn.pref_screen]
|
||||
for i in default_screen.allowed_depths:
|
||||
for v in i.visuals:
|
||||
if v.visual_id == default_screen.root_visual:
|
||||
return v
|
||||
|
||||
|
||||
def create_window(conn, width, height):
|
||||
"""Creates a window of the given dimensions and returns the XID"""
|
||||
wid = conn.generate_id()
|
||||
default_screen = conn.setup.roots[conn.pref_screen]
|
||||
|
||||
conn.core.CreateWindow(
|
||||
default_screen.root_depth, # depth
|
||||
wid, # id
|
||||
default_screen.root, # parent
|
||||
0, 0, width, height, 0, # x, y, w, h, border width
|
||||
xcffib.xproto.WindowClass.InputOutput, # window class
|
||||
default_screen.root_visual, # visual
|
||||
CW.BackPixel | CW.EventMask, # value mask
|
||||
[ # value list
|
||||
default_screen.black_pixel,
|
||||
EventMask.Exposure | EventMask.StructureNotify
|
||||
]
|
||||
)
|
||||
|
||||
return wid
|
||||
|
||||
|
||||
def create_pixmap(conn, wid, width, height):
|
||||
"""Creates a window of the given dimensions and returns the XID"""
|
||||
pixmap = conn.generate_id()
|
||||
default_screen = conn.setup.roots[conn.pref_screen]
|
||||
|
||||
conn.core.CreatePixmap(
|
||||
default_screen.root_depth, # depth
|
||||
pixmap, wid, # pixmap id, drawable id (window)
|
||||
width, height
|
||||
)
|
||||
|
||||
return pixmap
|
||||
|
||||
|
||||
def create_gc(conn):
|
||||
"""Creates a simple graphics context"""
|
||||
gc = conn.generate_id()
|
||||
default_screen = conn.setup.roots[conn.pref_screen]
|
||||
|
||||
conn.core.CreateGC(
|
||||
gc, default_screen.root, # gc id, drawable
|
||||
GC.Foreground | GC.Background, # value mask
|
||||
[ # value list
|
||||
default_screen.black_pixel,
|
||||
default_screen.white_pixel
|
||||
]
|
||||
)
|
||||
|
||||
return gc
|
||||
|
||||
|
||||
@pytest.mark.xfail(cairo_version() < 11200,
|
||||
reason="Cairo version too low")
|
||||
def test_xcb_pixmap(xcb_conn):
|
||||
width = 10
|
||||
height = 10
|
||||
|
||||
# create a new window
|
||||
wid = create_window(xcb_conn, width, height)
|
||||
# create the pixmap used to draw with cairo
|
||||
pixmap = create_pixmap(xcb_conn, wid, width, height)
|
||||
# create graphics context to copy pixmap on window
|
||||
gc = create_gc(xcb_conn)
|
||||
|
||||
# create XCB surface on pixmap
|
||||
root_visual = find_root_visual(xcb_conn)
|
||||
surface = XCBSurface(xcb_conn, pixmap, root_visual, width, height)
|
||||
assert surface
|
||||
|
||||
# use xcb surface to create context, draw white
|
||||
ctx = Context(surface)
|
||||
ctx.set_source_rgb(1, 1, 1)
|
||||
ctx.paint()
|
||||
|
||||
# map the window and wait for it to appear
|
||||
xcb_conn.core.MapWindow(wid)
|
||||
xcb_conn.flush()
|
||||
|
||||
start = time.time()
|
||||
while time.time() < start + 10:
|
||||
event = xcb_conn.wait_for_event()
|
||||
if isinstance(event, xcffib.xproto.ExposeEvent):
|
||||
break
|
||||
else:
|
||||
pytest.fail("Never received ExposeEvent")
|
||||
|
||||
# copy the pixmap to the window
|
||||
xcb_conn.core.CopyArea(
|
||||
pixmap, # source
|
||||
wid, # dest
|
||||
gc, # gc
|
||||
0, 0, # source x, source y
|
||||
0, 0, # dest x, dest y
|
||||
width, height
|
||||
)
|
||||
|
||||
ctx = None
|
||||
surface = None
|
||||
xcb_conn.core.FreeGC(gc)
|
||||
xcb_conn.core.FreePixmap(pixmap)
|
||||
|
||||
# flush the connection, make sure no errors were thrown
|
||||
xcb_conn.flush()
|
||||
while event:
|
||||
event = xcb_conn.poll_for_event()
|
||||
|
||||
|
||||
@pytest.mark.xfail(cairo_version() < 11200,
|
||||
reason="Cairo version too low")
|
||||
def test_xcb_window(xcb_conn):
|
||||
width = 10
|
||||
height = 10
|
||||
|
||||
# create a new window used to draw with cairo
|
||||
wid = create_window(xcb_conn, width, height)
|
||||
|
||||
# map the window and wait for it to appear
|
||||
xcb_conn.core.MapWindow(wid)
|
||||
xcb_conn.flush()
|
||||
|
||||
start = time.time()
|
||||
while time.time() < start + 10:
|
||||
event = xcb_conn.wait_for_event()
|
||||
if isinstance(event, xcffib.xproto.ExposeEvent):
|
||||
break
|
||||
else:
|
||||
pytest.fail("Never received ExposeEvent")
|
||||
|
||||
# create XCB surface on window
|
||||
root_visual = find_root_visual(xcb_conn)
|
||||
surface = XCBSurface(xcb_conn, wid, root_visual, width, height)
|
||||
assert surface
|
||||
|
||||
# use xcb surface to create context, draw white
|
||||
ctx = Context(surface)
|
||||
ctx.set_source_rgb(1, 1, 1)
|
||||
ctx.paint()
|
||||
|
||||
xcb_conn.flush()
|
||||
|
||||
# now move the window and change its size
|
||||
xcb_conn.core.ConfigureWindow(
|
||||
wid,
|
||||
(ConfigWindow.X | ConfigWindow.Y
|
||||
| ConfigWindow.Width | ConfigWindow.Height),
|
||||
[
|
||||
5, 5, # x, y
|
||||
width * 2, height * 2 # width, height
|
||||
]
|
||||
)
|
||||
xcb_conn.flush()
|
||||
|
||||
# wait for the notification of the size change
|
||||
start = time.time()
|
||||
while time.time() < start + 10:
|
||||
event = xcb_conn.wait_for_event()
|
||||
|
||||
if isinstance(event, xcffib.xproto.ConfigureNotifyEvent):
|
||||
assert event.width == 2*width
|
||||
assert event.height == 2*height
|
||||
width = event.width
|
||||
height = event.height
|
||||
break
|
||||
else:
|
||||
pytest.fail("Never received ConfigureNotifyEvent")
|
||||
|
||||
# re-size and re-draw the surface
|
||||
surface.set_size(width, height)
|
||||
ctx = Context(surface)
|
||||
ctx.set_source_rgb(1, 1, 1)
|
||||
ctx.paint()
|
||||
|
||||
# flush the connection, make sure no errors were thrown
|
||||
xcb_conn.flush()
|
||||
while event:
|
||||
event = xcb_conn.poll_for_event()
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
cairocffi.xcb
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Bindings for XCB surface objects using xcffib.
|
||||
|
||||
:copyright: Copyright 2014-2019 by Simon Sapin
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
from xcffib import visualtype_to_c_struct
|
||||
|
||||
from . import cairo, constants
|
||||
from .surfaces import SURFACE_TYPE_TO_CLASS, Surface
|
||||
|
||||
|
||||
class XCBSurface(Surface):
|
||||
"""The XCB surface is used to render cairo graphics to X Window System
|
||||
windows and pixmaps using the XCB library.
|
||||
|
||||
Creates a cairo surface that targets the given drawable (pixmap or window).
|
||||
|
||||
.. note::
|
||||
|
||||
This class works using objects and libraries in ``xcffib``.
|
||||
|
||||
:param conn: The ``xcffib.Connection`` for an open XCB connection
|
||||
:param drawable:
|
||||
An XID corresponding to an XCB drawable (a pixmap or a window)
|
||||
:param visual: An ``xcffib.xproto.VISUALTYPE`` object.
|
||||
:param width: integer
|
||||
:param height: integer
|
||||
"""
|
||||
def __init__(self, conn, drawable, visual, width, height):
|
||||
c_visual = visualtype_to_c_struct(visual)
|
||||
|
||||
p = cairo.cairo_xcb_surface_create(
|
||||
conn._conn, drawable, c_visual, width, height)
|
||||
Surface.__init__(self, p)
|
||||
|
||||
def set_size(self, width, height):
|
||||
"""
|
||||
Informs cairo of the new size of the X Drawable underlying the surface.
|
||||
For a surface created for a Window (rather than a Pixmap), this
|
||||
function must be called each time the size of the window changes (for
|
||||
a subwindow, you are normally resizing the window yourself, but for a
|
||||
toplevel window, it is necessary to listen for
|
||||
:class:`xcffib.xproto.ConfigureNotifyEvent`'s).
|
||||
|
||||
A Pixmap can never change size, so it is never necessary to call this
|
||||
function on a surface created for a Pixmap.
|
||||
|
||||
:param width: integer
|
||||
:param height: integer
|
||||
"""
|
||||
cairo.cairo_xcb_surface_set_size(self._pointer, width, height)
|
||||
self._check_status()
|
||||
|
||||
|
||||
SURFACE_TYPE_TO_CLASS[constants.SURFACE_TYPE_XCB] = XCBSurface
|
||||
Reference in New Issue
Block a user