Add existing to tracked

This commit is contained in:
Jay
2026-08-11 09:53:42 -04:00
parent afe07f3055
commit ffd6e3d73c
8531 changed files with 4396230 additions and 0 deletions
@@ -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.
cairoffis 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 faces 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 subclasss __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 cairos "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 faces family name."""
return ffi.string(cairo.cairo_toy_font_face_get_family(
self._pointer)).decode('utf8', 'replace')
def get_slant(self):
"""Return this font faces :ref:`FONT_SLANT` string."""
return cairo.cairo_toy_font_face_get_slant(self._pointer)
def get_weight(self):
"""Return this font faces :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 fonts 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 fonts 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 fonts 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 fonts 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 fonts 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 matrixs 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 patterns 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 subclasss __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 patterns 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 patterns 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 patterns 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 gradients 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 gradients 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 gradients 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 GdkPixbufs big-endian RGBA to cairos 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