Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- venv/lib/python3.10/site-packages/contourpy/__init__.py +285 -0
- venv/lib/python3.10/site-packages/contourpy/__pycache__/__init__.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/__pycache__/_version.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/__pycache__/array.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/__pycache__/chunk.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/__pycache__/convert.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/__pycache__/dechunk.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/__pycache__/enum_util.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/__pycache__/typecheck.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/__pycache__/types.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/_contourpy.pyi +199 -0
- venv/lib/python3.10/site-packages/contourpy/_version.py +1 -0
- venv/lib/python3.10/site-packages/contourpy/array.py +261 -0
- venv/lib/python3.10/site-packages/contourpy/chunk.py +95 -0
- venv/lib/python3.10/site-packages/contourpy/convert.py +621 -0
- venv/lib/python3.10/site-packages/contourpy/dechunk.py +207 -0
- venv/lib/python3.10/site-packages/contourpy/enum_util.py +57 -0
- venv/lib/python3.10/site-packages/contourpy/py.typed +0 -0
- venv/lib/python3.10/site-packages/contourpy/typecheck.py +203 -0
- venv/lib/python3.10/site-packages/contourpy/types.py +13 -0
- venv/lib/python3.10/site-packages/contourpy/util/__init__.py +5 -0
- venv/lib/python3.10/site-packages/contourpy/util/__pycache__/__init__.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/util/__pycache__/_build_config.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/util/__pycache__/bokeh_renderer.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/util/__pycache__/bokeh_util.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/util/__pycache__/data.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/util/__pycache__/mpl_renderer.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/util/__pycache__/mpl_util.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/util/__pycache__/renderer.cpython-310.pyc +0 -0
- venv/lib/python3.10/site-packages/contourpy/util/_build_config.py +60 -0
- venv/lib/python3.10/site-packages/contourpy/util/bokeh_renderer.py +339 -0
- venv/lib/python3.10/site-packages/contourpy/util/bokeh_util.py +74 -0
- venv/lib/python3.10/site-packages/contourpy/util/data.py +78 -0
- venv/lib/python3.10/site-packages/contourpy/util/mpl_renderer.py +535 -0
- venv/lib/python3.10/site-packages/contourpy/util/mpl_util.py +77 -0
- venv/lib/python3.10/site-packages/contourpy/util/renderer.py +166 -0
- venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/INSTALLER +1 -0
- venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/METADATA +42 -0
- venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/RECORD +25 -0
- venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/REQUESTED +0 -0
- venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/WHEEL +4 -0
- venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/licenses/LICENSE +24 -0
- venv/lib/python3.10/site-packages/fontTools/__init__.py +8 -0
- venv/lib/python3.10/site-packages/fontTools/__main__.py +35 -0
- venv/lib/python3.10/site-packages/fontTools/afmLib.py +439 -0
- venv/lib/python3.10/site-packages/fontTools/agl.py +0 -0
- venv/lib/python3.10/site-packages/fontTools/annotations.py +30 -0
- venv/lib/python3.10/site-packages/fontTools/cffLib/CFF2ToCFF.py +233 -0
- venv/lib/python3.10/site-packages/fontTools/cffLib/CFFToCFF2.py +305 -0
- venv/lib/python3.10/site-packages/fontTools/cffLib/__init__.py +0 -0
venv/lib/python3.10/site-packages/contourpy/__init__.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
from contourpy._contourpy import (
|
| 8 |
+
ContourGenerator,
|
| 9 |
+
FillType,
|
| 10 |
+
LineType,
|
| 11 |
+
Mpl2005ContourGenerator,
|
| 12 |
+
Mpl2014ContourGenerator,
|
| 13 |
+
SerialContourGenerator,
|
| 14 |
+
ThreadedContourGenerator,
|
| 15 |
+
ZInterp,
|
| 16 |
+
max_threads,
|
| 17 |
+
)
|
| 18 |
+
from contourpy._version import __version__
|
| 19 |
+
from contourpy.chunk import calc_chunk_sizes
|
| 20 |
+
from contourpy.convert import (
|
| 21 |
+
convert_filled,
|
| 22 |
+
convert_lines,
|
| 23 |
+
convert_multi_filled,
|
| 24 |
+
convert_multi_lines,
|
| 25 |
+
)
|
| 26 |
+
from contourpy.dechunk import (
|
| 27 |
+
dechunk_filled,
|
| 28 |
+
dechunk_lines,
|
| 29 |
+
dechunk_multi_filled,
|
| 30 |
+
dechunk_multi_lines,
|
| 31 |
+
)
|
| 32 |
+
from contourpy.enum_util import as_fill_type, as_line_type, as_z_interp
|
| 33 |
+
|
| 34 |
+
if TYPE_CHECKING:
|
| 35 |
+
from typing import Any
|
| 36 |
+
|
| 37 |
+
from numpy.typing import ArrayLike
|
| 38 |
+
|
| 39 |
+
from ._contourpy import CoordinateArray, MaskArray
|
| 40 |
+
|
| 41 |
+
__all__ = [
|
| 42 |
+
"__version__",
|
| 43 |
+
"contour_generator",
|
| 44 |
+
"convert_filled",
|
| 45 |
+
"convert_lines",
|
| 46 |
+
"convert_multi_filled",
|
| 47 |
+
"convert_multi_lines",
|
| 48 |
+
"dechunk_filled",
|
| 49 |
+
"dechunk_lines",
|
| 50 |
+
"dechunk_multi_filled",
|
| 51 |
+
"dechunk_multi_lines",
|
| 52 |
+
"max_threads",
|
| 53 |
+
"FillType",
|
| 54 |
+
"LineType",
|
| 55 |
+
"ContourGenerator",
|
| 56 |
+
"Mpl2005ContourGenerator",
|
| 57 |
+
"Mpl2014ContourGenerator",
|
| 58 |
+
"SerialContourGenerator",
|
| 59 |
+
"ThreadedContourGenerator",
|
| 60 |
+
"ZInterp",
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# Simple mapping of algorithm name to class name.
|
| 65 |
+
_class_lookup: dict[str, type[ContourGenerator]] = {
|
| 66 |
+
"mpl2005": Mpl2005ContourGenerator,
|
| 67 |
+
"mpl2014": Mpl2014ContourGenerator,
|
| 68 |
+
"serial": SerialContourGenerator,
|
| 69 |
+
"threaded": ThreadedContourGenerator,
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _remove_z_mask(
|
| 74 |
+
z: ArrayLike | np.ma.MaskedArray[Any, Any] | None,
|
| 75 |
+
) -> tuple[CoordinateArray, MaskArray | None]:
|
| 76 |
+
# Preserve mask if present.
|
| 77 |
+
z_array = np.ma.asarray(z, dtype=np.float64) # type: ignore[no-untyped-call]
|
| 78 |
+
z_masked = np.ma.masked_invalid(z_array, copy=False) # type: ignore[no-untyped-call]
|
| 79 |
+
|
| 80 |
+
if np.ma.is_masked(z_masked): # type: ignore[no-untyped-call]
|
| 81 |
+
mask = np.ma.getmask(z_masked) # type: ignore[no-untyped-call]
|
| 82 |
+
else:
|
| 83 |
+
mask = None
|
| 84 |
+
|
| 85 |
+
return np.ma.getdata(z_masked), mask # type: ignore[no-untyped-call]
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def contour_generator(
|
| 89 |
+
x: ArrayLike | None = None,
|
| 90 |
+
y: ArrayLike | None = None,
|
| 91 |
+
z: ArrayLike | np.ma.MaskedArray[Any, Any] | None = None,
|
| 92 |
+
*,
|
| 93 |
+
name: str = "serial",
|
| 94 |
+
corner_mask: bool | None = None,
|
| 95 |
+
line_type: LineType | str | None = None,
|
| 96 |
+
fill_type: FillType | str | None = None,
|
| 97 |
+
chunk_size: int | tuple[int, int] | None = None,
|
| 98 |
+
chunk_count: int | tuple[int, int] | None = None,
|
| 99 |
+
total_chunk_count: int | None = None,
|
| 100 |
+
quad_as_tri: bool = False,
|
| 101 |
+
z_interp: ZInterp | str | None = ZInterp.Linear,
|
| 102 |
+
thread_count: int = 0,
|
| 103 |
+
) -> ContourGenerator:
|
| 104 |
+
"""Create and return a :class:`~.ContourGenerator` object.
|
| 105 |
+
|
| 106 |
+
The class and properties of the returned :class:`~.ContourGenerator` are determined by the
|
| 107 |
+
function arguments, with sensible defaults.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
x (array-like of shape (ny, nx) or (nx,), optional): The x-coordinates of the ``z`` values.
|
| 111 |
+
May be 2D with the same shape as ``z.shape``, or 1D with length ``nx = z.shape[1]``.
|
| 112 |
+
If not specified are assumed to be ``np.arange(nx)``. Must be ordered monotonically.
|
| 113 |
+
y (array-like of shape (ny, nx) or (ny,), optional): The y-coordinates of the ``z`` values.
|
| 114 |
+
May be 2D with the same shape as ``z.shape``, or 1D with length ``ny = z.shape[0]``.
|
| 115 |
+
If not specified are assumed to be ``np.arange(ny)``. Must be ordered monotonically.
|
| 116 |
+
z (array-like of shape (ny, nx), may be a masked array): The 2D gridded values to calculate
|
| 117 |
+
the contours of. May be a masked array, and any invalid values (``np.inf`` or
|
| 118 |
+
``np.nan``) will also be masked out.
|
| 119 |
+
name (str): Algorithm name, one of ``"serial"``, ``"threaded"``, ``"mpl2005"`` or
|
| 120 |
+
``"mpl2014"``, default ``"serial"``.
|
| 121 |
+
corner_mask (bool, optional): Enable/disable corner masking, which only has an effect if
|
| 122 |
+
``z`` is a masked array. If ``False``, any quad touching a masked point is masked out.
|
| 123 |
+
If ``True``, only the triangular corners of quads nearest these points are always masked
|
| 124 |
+
out, other triangular corners comprising three unmasked points are contoured as usual.
|
| 125 |
+
If not specified, uses the default provided by the algorithm ``name``.
|
| 126 |
+
line_type (LineType or str, optional): The format of contour line data returned from calls
|
| 127 |
+
to :meth:`~.ContourGenerator.lines`, specified either as a :class:`~.LineType` or its
|
| 128 |
+
string equivalent such as ``"SeparateCode"``.
|
| 129 |
+
If not specified, uses the default provided by the algorithm ``name``.
|
| 130 |
+
The relationship between the :class:`~.LineType` enum and the data format returned from
|
| 131 |
+
:meth:`~.ContourGenerator.lines` is explained at :ref:`line_type`.
|
| 132 |
+
fill_type (FillType or str, optional): The format of filled contour data returned from calls
|
| 133 |
+
to :meth:`~.ContourGenerator.filled`, specified either as a :class:`~.FillType` or its
|
| 134 |
+
string equivalent such as ``"OuterOffset"``.
|
| 135 |
+
If not specified, uses the default provided by the algorithm ``name``.
|
| 136 |
+
The relationship between the :class:`~.FillType` enum and the data format returned from
|
| 137 |
+
:meth:`~.ContourGenerator.filled` is explained at :ref:`fill_type`.
|
| 138 |
+
chunk_size (int or tuple(int, int), optional): Chunk size in (y, x) directions, or the same
|
| 139 |
+
size in both directions if only one value is specified.
|
| 140 |
+
chunk_count (int or tuple(int, int), optional): Chunk count in (y, x) directions, or the
|
| 141 |
+
same count in both directions if only one value is specified.
|
| 142 |
+
total_chunk_count (int, optional): Total number of chunks.
|
| 143 |
+
quad_as_tri (bool): Enable/disable treating quads as 4 triangles, default ``False``.
|
| 144 |
+
If ``False``, a contour line within a quad is a straight line between points on two of
|
| 145 |
+
its edges. If ``True``, each full quad is divided into 4 triangles using a virtual point
|
| 146 |
+
at the centre (mean x, y of the corner points) and a contour line is piecewise linear
|
| 147 |
+
within those triangles. Corner-masked triangles are not affected by this setting, only
|
| 148 |
+
full unmasked quads.
|
| 149 |
+
z_interp (ZInterp or str, optional): How to interpolate ``z`` values when determining where
|
| 150 |
+
contour lines intersect the edges of quads and the ``z`` values of the central points of
|
| 151 |
+
quads, specified either as a :class:`~contourpy.ZInterp` or its string equivalent such
|
| 152 |
+
as ``"Log"``. Default is ``ZInterp.Linear``.
|
| 153 |
+
thread_count (int): Number of threads to use for contour calculation, default 0. Threads can
|
| 154 |
+
only be used with an algorithm ``name`` that supports threads (currently only
|
| 155 |
+
``name="threaded"``) and there must be at least the same number of chunks as threads.
|
| 156 |
+
If ``thread_count=0`` and ``name="threaded"`` then it uses the maximum number of threads
|
| 157 |
+
as determined by the C++11 call ``std::thread::hardware_concurrency()``. If ``name`` is
|
| 158 |
+
something other than ``"threaded"`` then the ``thread_count`` will be set to ``1``.
|
| 159 |
+
|
| 160 |
+
Return:
|
| 161 |
+
:class:`~.ContourGenerator`.
|
| 162 |
+
|
| 163 |
+
Note:
|
| 164 |
+
A maximum of one of ``chunk_size``, ``chunk_count`` and ``total_chunk_count`` may be
|
| 165 |
+
specified.
|
| 166 |
+
|
| 167 |
+
Warning:
|
| 168 |
+
The ``name="mpl2005"`` algorithm does not implement chunking for contour lines.
|
| 169 |
+
"""
|
| 170 |
+
x = np.asarray(x, dtype=np.float64)
|
| 171 |
+
y = np.asarray(y, dtype=np.float64)
|
| 172 |
+
z, mask = _remove_z_mask(z)
|
| 173 |
+
|
| 174 |
+
# Check arguments: z.
|
| 175 |
+
if z.ndim != 2:
|
| 176 |
+
raise TypeError(f"Input z must be 2D, not {z.ndim}D")
|
| 177 |
+
|
| 178 |
+
if z.shape[0] < 2 or z.shape[1] < 2:
|
| 179 |
+
raise TypeError(f"Input z must be at least a (2, 2) shaped array, but has shape {z.shape}")
|
| 180 |
+
|
| 181 |
+
ny, nx = z.shape
|
| 182 |
+
|
| 183 |
+
# Check arguments: x and y.
|
| 184 |
+
if x.ndim != y.ndim:
|
| 185 |
+
raise TypeError(f"Number of dimensions of x ({x.ndim}) and y ({y.ndim}) do not match")
|
| 186 |
+
|
| 187 |
+
if x.ndim == 0:
|
| 188 |
+
x = np.arange(nx, dtype=np.float64)
|
| 189 |
+
y = np.arange(ny, dtype=np.float64)
|
| 190 |
+
x, y = np.meshgrid(x, y)
|
| 191 |
+
elif x.ndim == 1:
|
| 192 |
+
if len(x) != nx:
|
| 193 |
+
raise TypeError(f"Length of x ({len(x)}) must match number of columns in z ({nx})")
|
| 194 |
+
if len(y) != ny:
|
| 195 |
+
raise TypeError(f"Length of y ({len(y)}) must match number of rows in z ({ny})")
|
| 196 |
+
x, y = np.meshgrid(x, y)
|
| 197 |
+
elif x.ndim == 2:
|
| 198 |
+
if x.shape != z.shape:
|
| 199 |
+
raise TypeError(f"Shapes of x {x.shape} and z {z.shape} do not match")
|
| 200 |
+
if y.shape != z.shape:
|
| 201 |
+
raise TypeError(f"Shapes of y {y.shape} and z {z.shape} do not match")
|
| 202 |
+
else:
|
| 203 |
+
raise TypeError(f"Inputs x and y must be None, 1D or 2D, not {x.ndim}D")
|
| 204 |
+
|
| 205 |
+
# Check mask shape just in case.
|
| 206 |
+
if mask is not None and mask.shape != z.shape:
|
| 207 |
+
raise ValueError("If mask is set it must be a 2D array with the same shape as z")
|
| 208 |
+
|
| 209 |
+
# Check arguments: name.
|
| 210 |
+
if name not in _class_lookup:
|
| 211 |
+
raise ValueError(f"Unrecognised contour generator name: {name}")
|
| 212 |
+
|
| 213 |
+
# Check arguments: chunk_size, chunk_count and total_chunk_count.
|
| 214 |
+
y_chunk_size, x_chunk_size = calc_chunk_sizes(
|
| 215 |
+
chunk_size, chunk_count, total_chunk_count, ny, nx)
|
| 216 |
+
|
| 217 |
+
cls = _class_lookup[name]
|
| 218 |
+
|
| 219 |
+
# Check arguments: corner_mask.
|
| 220 |
+
if corner_mask is None:
|
| 221 |
+
# Set it to default, which is True if the algorithm supports it.
|
| 222 |
+
corner_mask = cls.supports_corner_mask()
|
| 223 |
+
elif corner_mask and not cls.supports_corner_mask():
|
| 224 |
+
raise ValueError(f"{name} contour generator does not support corner_mask=True")
|
| 225 |
+
|
| 226 |
+
# Check arguments: line_type.
|
| 227 |
+
if line_type is None:
|
| 228 |
+
line_type = cls.default_line_type
|
| 229 |
+
else:
|
| 230 |
+
line_type = as_line_type(line_type)
|
| 231 |
+
|
| 232 |
+
if not cls.supports_line_type(line_type):
|
| 233 |
+
raise ValueError(f"{name} contour generator does not support line_type {line_type}")
|
| 234 |
+
|
| 235 |
+
# Check arguments: fill_type.
|
| 236 |
+
if fill_type is None:
|
| 237 |
+
fill_type = cls.default_fill_type
|
| 238 |
+
else:
|
| 239 |
+
fill_type = as_fill_type(fill_type)
|
| 240 |
+
|
| 241 |
+
if not cls.supports_fill_type(fill_type):
|
| 242 |
+
raise ValueError(f"{name} contour generator does not support fill_type {fill_type}")
|
| 243 |
+
|
| 244 |
+
# Check arguments: quad_as_tri.
|
| 245 |
+
if quad_as_tri and not cls.supports_quad_as_tri():
|
| 246 |
+
raise ValueError(f"{name} contour generator does not support quad_as_tri=True")
|
| 247 |
+
|
| 248 |
+
# Check arguments: z_interp.
|
| 249 |
+
if z_interp is None:
|
| 250 |
+
z_interp = ZInterp.Linear
|
| 251 |
+
else:
|
| 252 |
+
z_interp = as_z_interp(z_interp)
|
| 253 |
+
|
| 254 |
+
if z_interp != ZInterp.Linear and not cls.supports_z_interp():
|
| 255 |
+
raise ValueError(f"{name} contour generator does not support z_interp {z_interp}")
|
| 256 |
+
|
| 257 |
+
# Check arguments: thread_count.
|
| 258 |
+
if thread_count not in (0, 1) and not cls.supports_threads():
|
| 259 |
+
raise ValueError(f"{name} contour generator does not support thread_count {thread_count}")
|
| 260 |
+
|
| 261 |
+
# Prepare args and kwargs for contour generator constructor.
|
| 262 |
+
args = [x, y, z, mask]
|
| 263 |
+
kwargs: dict[str, int | bool | LineType | FillType | ZInterp] = {
|
| 264 |
+
"x_chunk_size": x_chunk_size,
|
| 265 |
+
"y_chunk_size": y_chunk_size,
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
if name not in ("mpl2005", "mpl2014"):
|
| 269 |
+
kwargs["line_type"] = line_type
|
| 270 |
+
kwargs["fill_type"] = fill_type
|
| 271 |
+
|
| 272 |
+
if cls.supports_corner_mask():
|
| 273 |
+
kwargs["corner_mask"] = corner_mask
|
| 274 |
+
|
| 275 |
+
if cls.supports_quad_as_tri():
|
| 276 |
+
kwargs["quad_as_tri"] = quad_as_tri
|
| 277 |
+
|
| 278 |
+
if cls.supports_z_interp():
|
| 279 |
+
kwargs["z_interp"] = z_interp
|
| 280 |
+
|
| 281 |
+
if cls.supports_threads():
|
| 282 |
+
kwargs["thread_count"] = thread_count
|
| 283 |
+
|
| 284 |
+
# Create contour generator.
|
| 285 |
+
return cls(*args, **kwargs)
|
venv/lib/python3.10/site-packages/contourpy/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (9.75 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/__pycache__/_version.cpython-310.pyc
ADDED
|
Binary file (195 Bytes). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/__pycache__/array.cpython-310.pyc
ADDED
|
Binary file (9.58 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/__pycache__/chunk.cpython-310.pyc
ADDED
|
Binary file (2.79 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/__pycache__/convert.cpython-310.pyc
ADDED
|
Binary file (16.7 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/__pycache__/dechunk.cpython-310.pyc
ADDED
|
Binary file (6.22 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/__pycache__/enum_util.cpython-310.pyc
ADDED
|
Binary file (1.6 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/__pycache__/typecheck.cpython-310.pyc
ADDED
|
Binary file (6.59 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/__pycache__/types.cpython-310.pyc
ADDED
|
Binary file (385 Bytes). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/_contourpy.pyi
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import ClassVar, NoReturn, TypeAlias
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import numpy.typing as npt
|
| 5 |
+
|
| 6 |
+
import contourpy._contourpy as cpy
|
| 7 |
+
|
| 8 |
+
# Input numpy array types, the same as in common.h
|
| 9 |
+
CoordinateArray: TypeAlias = npt.NDArray[np.float64]
|
| 10 |
+
MaskArray: TypeAlias = npt.NDArray[np.bool_]
|
| 11 |
+
LevelArray: TypeAlias = npt.ArrayLike
|
| 12 |
+
|
| 13 |
+
# Output numpy array types, the same as in common.h
|
| 14 |
+
PointArray: TypeAlias = npt.NDArray[np.float64]
|
| 15 |
+
CodeArray: TypeAlias = npt.NDArray[np.uint8]
|
| 16 |
+
OffsetArray: TypeAlias = npt.NDArray[np.uint32]
|
| 17 |
+
|
| 18 |
+
# Types returned from filled()
|
| 19 |
+
FillReturn_OuterCode: TypeAlias = tuple[list[PointArray], list[CodeArray]]
|
| 20 |
+
FillReturn_OuterOffset: TypeAlias = tuple[list[PointArray], list[OffsetArray]]
|
| 21 |
+
FillReturn_ChunkCombinedCode: TypeAlias = tuple[list[PointArray | None], list[CodeArray | None]]
|
| 22 |
+
FillReturn_ChunkCombinedOffset: TypeAlias = tuple[list[PointArray | None], list[OffsetArray | None]]
|
| 23 |
+
FillReturn_ChunkCombinedCodeOffset: TypeAlias = tuple[list[PointArray | None], list[CodeArray | None], list[OffsetArray | None]]
|
| 24 |
+
FillReturn_ChunkCombinedOffsetOffset: TypeAlias = tuple[list[PointArray | None], list[OffsetArray | None], list[OffsetArray | None]]
|
| 25 |
+
FillReturn_Chunk: TypeAlias = FillReturn_ChunkCombinedCode | FillReturn_ChunkCombinedOffset | FillReturn_ChunkCombinedCodeOffset | FillReturn_ChunkCombinedOffsetOffset
|
| 26 |
+
FillReturn: TypeAlias = FillReturn_OuterCode | FillReturn_OuterOffset | FillReturn_Chunk
|
| 27 |
+
|
| 28 |
+
# Types returned from lines()
|
| 29 |
+
LineReturn_Separate: TypeAlias = list[PointArray]
|
| 30 |
+
LineReturn_SeparateCode: TypeAlias = tuple[list[PointArray], list[CodeArray]]
|
| 31 |
+
LineReturn_ChunkCombinedCode: TypeAlias = tuple[list[PointArray | None], list[CodeArray | None]]
|
| 32 |
+
LineReturn_ChunkCombinedOffset: TypeAlias = tuple[list[PointArray | None], list[OffsetArray | None]]
|
| 33 |
+
LineReturn_ChunkCombinedNan: TypeAlias = tuple[list[PointArray | None]]
|
| 34 |
+
LineReturn_Chunk: TypeAlias = LineReturn_ChunkCombinedCode | LineReturn_ChunkCombinedOffset | LineReturn_ChunkCombinedNan
|
| 35 |
+
LineReturn: TypeAlias = LineReturn_Separate | LineReturn_SeparateCode | LineReturn_Chunk
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
NDEBUG: int
|
| 39 |
+
__version__: str
|
| 40 |
+
|
| 41 |
+
class FillType:
|
| 42 |
+
ChunkCombinedCode: ClassVar[cpy.FillType]
|
| 43 |
+
ChunkCombinedCodeOffset: ClassVar[cpy.FillType]
|
| 44 |
+
ChunkCombinedOffset: ClassVar[cpy.FillType]
|
| 45 |
+
ChunkCombinedOffsetOffset: ClassVar[cpy.FillType]
|
| 46 |
+
OuterCode: ClassVar[cpy.FillType]
|
| 47 |
+
OuterOffset: ClassVar[cpy.FillType]
|
| 48 |
+
__members__: ClassVar[dict[str, cpy.FillType]]
|
| 49 |
+
def __eq__(self, other: object) -> bool: ...
|
| 50 |
+
def __getstate__(self) -> int: ...
|
| 51 |
+
def __hash__(self) -> int: ...
|
| 52 |
+
def __index__(self) -> int: ...
|
| 53 |
+
def __init__(self, value: int) -> None: ...
|
| 54 |
+
def __int__(self) -> int: ...
|
| 55 |
+
def __ne__(self, other: object) -> bool: ...
|
| 56 |
+
def __setstate__(self, state: int) -> NoReturn: ...
|
| 57 |
+
@property
|
| 58 |
+
def name(self) -> str: ...
|
| 59 |
+
@property
|
| 60 |
+
def value(self) -> int: ...
|
| 61 |
+
|
| 62 |
+
class LineType:
|
| 63 |
+
ChunkCombinedCode: ClassVar[cpy.LineType]
|
| 64 |
+
ChunkCombinedNan: ClassVar[cpy.LineType]
|
| 65 |
+
ChunkCombinedOffset: ClassVar[cpy.LineType]
|
| 66 |
+
Separate: ClassVar[cpy.LineType]
|
| 67 |
+
SeparateCode: ClassVar[cpy.LineType]
|
| 68 |
+
__members__: ClassVar[dict[str, cpy.LineType]]
|
| 69 |
+
def __eq__(self, other: object) -> bool: ...
|
| 70 |
+
def __getstate__(self) -> int: ...
|
| 71 |
+
def __hash__(self) -> int: ...
|
| 72 |
+
def __index__(self) -> int: ...
|
| 73 |
+
def __init__(self, value: int) -> None: ...
|
| 74 |
+
def __int__(self) -> int: ...
|
| 75 |
+
def __ne__(self, other: object) -> bool: ...
|
| 76 |
+
def __setstate__(self, state: int) -> NoReturn: ...
|
| 77 |
+
@property
|
| 78 |
+
def name(self) -> str: ...
|
| 79 |
+
@property
|
| 80 |
+
def value(self) -> int: ...
|
| 81 |
+
|
| 82 |
+
class ZInterp:
|
| 83 |
+
Linear: ClassVar[cpy.ZInterp]
|
| 84 |
+
Log: ClassVar[cpy.ZInterp]
|
| 85 |
+
__members__: ClassVar[dict[str, cpy.ZInterp]]
|
| 86 |
+
def __eq__(self, other: object) -> bool: ...
|
| 87 |
+
def __getstate__(self) -> int: ...
|
| 88 |
+
def __hash__(self) -> int: ...
|
| 89 |
+
def __index__(self) -> int: ...
|
| 90 |
+
def __init__(self, value: int) -> None: ...
|
| 91 |
+
def __int__(self) -> int: ...
|
| 92 |
+
def __ne__(self, other: object) -> bool: ...
|
| 93 |
+
def __setstate__(self, state: int) -> NoReturn: ...
|
| 94 |
+
@property
|
| 95 |
+
def name(self) -> str: ...
|
| 96 |
+
@property
|
| 97 |
+
def value(self) -> int: ...
|
| 98 |
+
|
| 99 |
+
def max_threads() -> int: ...
|
| 100 |
+
|
| 101 |
+
class ContourGenerator:
|
| 102 |
+
def create_contour(self, level: float) -> LineReturn: ...
|
| 103 |
+
def create_filled_contour(self, lower_level: float, upper_level: float) -> FillReturn: ...
|
| 104 |
+
def filled(self, lower_level: float, upper_level: float) -> FillReturn: ...
|
| 105 |
+
def lines(self, level: float) -> LineReturn: ...
|
| 106 |
+
def multi_filled(self, levels: LevelArray) -> list[FillReturn]: ...
|
| 107 |
+
def multi_lines(self, levels: LevelArray) -> list[LineReturn]: ...
|
| 108 |
+
@staticmethod
|
| 109 |
+
def supports_corner_mask() -> bool: ...
|
| 110 |
+
@staticmethod
|
| 111 |
+
def supports_fill_type(fill_type: FillType) -> bool: ...
|
| 112 |
+
@staticmethod
|
| 113 |
+
def supports_line_type(line_type: LineType) -> bool: ...
|
| 114 |
+
@staticmethod
|
| 115 |
+
def supports_quad_as_tri() -> bool: ...
|
| 116 |
+
@staticmethod
|
| 117 |
+
def supports_threads() -> bool: ...
|
| 118 |
+
@staticmethod
|
| 119 |
+
def supports_z_interp() -> bool: ...
|
| 120 |
+
@property
|
| 121 |
+
def chunk_count(self) -> tuple[int, int]: ...
|
| 122 |
+
@property
|
| 123 |
+
def chunk_size(self) -> tuple[int, int]: ...
|
| 124 |
+
@property
|
| 125 |
+
def corner_mask(self) -> bool: ...
|
| 126 |
+
@property
|
| 127 |
+
def fill_type(self) -> FillType: ...
|
| 128 |
+
@property
|
| 129 |
+
def line_type(self) -> LineType: ...
|
| 130 |
+
@property
|
| 131 |
+
def quad_as_tri(self) -> bool: ...
|
| 132 |
+
@property
|
| 133 |
+
def thread_count(self) -> int: ...
|
| 134 |
+
@property
|
| 135 |
+
def z_interp(self) -> ZInterp: ...
|
| 136 |
+
default_fill_type: cpy.FillType
|
| 137 |
+
default_line_type: cpy.LineType
|
| 138 |
+
|
| 139 |
+
class Mpl2005ContourGenerator(ContourGenerator):
|
| 140 |
+
def __init__(
|
| 141 |
+
self,
|
| 142 |
+
x: CoordinateArray,
|
| 143 |
+
y: CoordinateArray,
|
| 144 |
+
z: CoordinateArray,
|
| 145 |
+
mask: MaskArray,
|
| 146 |
+
*,
|
| 147 |
+
x_chunk_size: int = 0,
|
| 148 |
+
y_chunk_size: int = 0,
|
| 149 |
+
) -> None: ...
|
| 150 |
+
|
| 151 |
+
class Mpl2014ContourGenerator(ContourGenerator):
|
| 152 |
+
def __init__(
|
| 153 |
+
self,
|
| 154 |
+
x: CoordinateArray,
|
| 155 |
+
y: CoordinateArray,
|
| 156 |
+
z: CoordinateArray,
|
| 157 |
+
mask: MaskArray,
|
| 158 |
+
*,
|
| 159 |
+
corner_mask: bool,
|
| 160 |
+
x_chunk_size: int = 0,
|
| 161 |
+
y_chunk_size: int = 0,
|
| 162 |
+
) -> None: ...
|
| 163 |
+
|
| 164 |
+
class SerialContourGenerator(ContourGenerator):
|
| 165 |
+
def __init__(
|
| 166 |
+
self,
|
| 167 |
+
x: CoordinateArray,
|
| 168 |
+
y: CoordinateArray,
|
| 169 |
+
z: CoordinateArray,
|
| 170 |
+
mask: MaskArray,
|
| 171 |
+
*,
|
| 172 |
+
corner_mask: bool,
|
| 173 |
+
line_type: LineType,
|
| 174 |
+
fill_type: FillType,
|
| 175 |
+
quad_as_tri: bool,
|
| 176 |
+
z_interp: ZInterp,
|
| 177 |
+
x_chunk_size: int = 0,
|
| 178 |
+
y_chunk_size: int = 0,
|
| 179 |
+
) -> None: ...
|
| 180 |
+
def _write_cache(self) -> NoReturn: ...
|
| 181 |
+
|
| 182 |
+
class ThreadedContourGenerator(ContourGenerator):
|
| 183 |
+
def __init__(
|
| 184 |
+
self,
|
| 185 |
+
x: CoordinateArray,
|
| 186 |
+
y: CoordinateArray,
|
| 187 |
+
z: CoordinateArray,
|
| 188 |
+
mask: MaskArray,
|
| 189 |
+
*,
|
| 190 |
+
corner_mask: bool,
|
| 191 |
+
line_type: LineType,
|
| 192 |
+
fill_type: FillType,
|
| 193 |
+
quad_as_tri: bool,
|
| 194 |
+
z_interp: ZInterp,
|
| 195 |
+
x_chunk_size: int = 0,
|
| 196 |
+
y_chunk_size: int = 0,
|
| 197 |
+
thread_count: int = 0,
|
| 198 |
+
) -> None: ...
|
| 199 |
+
def _write_cache(self) -> None: ...
|
venv/lib/python3.10/site-packages/contourpy/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
__version__ = "1.3.2"
|
venv/lib/python3.10/site-packages/contourpy/array.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from itertools import chain, pairwise
|
| 4 |
+
from typing import TYPE_CHECKING
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
from contourpy.typecheck import check_code_array, check_offset_array, check_point_array
|
| 9 |
+
from contourpy.types import CLOSEPOLY, LINETO, MOVETO, code_dtype, offset_dtype, point_dtype
|
| 10 |
+
|
| 11 |
+
if TYPE_CHECKING:
|
| 12 |
+
import contourpy._contourpy as cpy
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def codes_from_offsets(offsets: cpy.OffsetArray) -> cpy.CodeArray:
|
| 16 |
+
"""Determine codes from offsets, assuming they all correspond to closed polygons.
|
| 17 |
+
"""
|
| 18 |
+
check_offset_array(offsets)
|
| 19 |
+
|
| 20 |
+
n = offsets[-1]
|
| 21 |
+
codes = np.full(n, LINETO, dtype=code_dtype)
|
| 22 |
+
codes[offsets[:-1]] = MOVETO
|
| 23 |
+
codes[offsets[1:] - 1] = CLOSEPOLY
|
| 24 |
+
return codes
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def codes_from_offsets_and_points(
|
| 28 |
+
offsets: cpy.OffsetArray,
|
| 29 |
+
points: cpy.PointArray,
|
| 30 |
+
) -> cpy.CodeArray:
|
| 31 |
+
"""Determine codes from offsets and points, using the equality of the start and end points of
|
| 32 |
+
each line to determine if lines are closed or not.
|
| 33 |
+
"""
|
| 34 |
+
check_offset_array(offsets)
|
| 35 |
+
check_point_array(points)
|
| 36 |
+
|
| 37 |
+
codes = np.full(len(points), LINETO, dtype=code_dtype)
|
| 38 |
+
codes[offsets[:-1]] = MOVETO
|
| 39 |
+
|
| 40 |
+
end_offsets = offsets[1:] - 1
|
| 41 |
+
closed = np.all(points[offsets[:-1]] == points[end_offsets], axis=1)
|
| 42 |
+
codes[end_offsets[closed]] = CLOSEPOLY
|
| 43 |
+
|
| 44 |
+
return codes
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def codes_from_points(points: cpy.PointArray) -> cpy.CodeArray:
|
| 48 |
+
"""Determine codes for a single line, using the equality of the start and end points to
|
| 49 |
+
determine if the line is closed or not.
|
| 50 |
+
"""
|
| 51 |
+
check_point_array(points)
|
| 52 |
+
|
| 53 |
+
n = len(points)
|
| 54 |
+
codes = np.full(n, LINETO, dtype=code_dtype)
|
| 55 |
+
codes[0] = MOVETO
|
| 56 |
+
if np.all(points[0] == points[-1]):
|
| 57 |
+
codes[-1] = CLOSEPOLY
|
| 58 |
+
return codes
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def concat_codes(list_of_codes: list[cpy.CodeArray]) -> cpy.CodeArray:
|
| 62 |
+
"""Concatenate a list of codes arrays into a single code array.
|
| 63 |
+
"""
|
| 64 |
+
if not list_of_codes:
|
| 65 |
+
raise ValueError("Empty list passed to concat_codes")
|
| 66 |
+
|
| 67 |
+
return np.concatenate(list_of_codes, dtype=code_dtype)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def concat_codes_or_none(list_of_codes_or_none: list[cpy.CodeArray | None]) -> cpy.CodeArray | None:
|
| 71 |
+
"""Concatenate a list of codes arrays or None into a single code array or None.
|
| 72 |
+
"""
|
| 73 |
+
list_of_codes = [codes for codes in list_of_codes_or_none if codes is not None]
|
| 74 |
+
if list_of_codes:
|
| 75 |
+
return concat_codes(list_of_codes)
|
| 76 |
+
else:
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def concat_offsets(list_of_offsets: list[cpy.OffsetArray]) -> cpy.OffsetArray:
|
| 81 |
+
"""Concatenate a list of offsets arrays into a single offset array.
|
| 82 |
+
"""
|
| 83 |
+
if not list_of_offsets:
|
| 84 |
+
raise ValueError("Empty list passed to concat_offsets")
|
| 85 |
+
|
| 86 |
+
n = len(list_of_offsets)
|
| 87 |
+
cumulative = np.cumsum([offsets[-1] for offsets in list_of_offsets], dtype=offset_dtype)
|
| 88 |
+
ret: cpy.OffsetArray = np.concatenate(
|
| 89 |
+
(list_of_offsets[0], *(list_of_offsets[i+1][1:] + cumulative[i] for i in range(n-1))),
|
| 90 |
+
dtype=offset_dtype,
|
| 91 |
+
)
|
| 92 |
+
return ret
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def concat_offsets_or_none(
|
| 96 |
+
list_of_offsets_or_none: list[cpy.OffsetArray | None],
|
| 97 |
+
) -> cpy.OffsetArray | None:
|
| 98 |
+
"""Concatenate a list of offsets arrays or None into a single offset array or None.
|
| 99 |
+
"""
|
| 100 |
+
list_of_offsets = [offsets for offsets in list_of_offsets_or_none if offsets is not None]
|
| 101 |
+
if list_of_offsets:
|
| 102 |
+
return concat_offsets(list_of_offsets)
|
| 103 |
+
else:
|
| 104 |
+
return None
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def concat_points(list_of_points: list[cpy.PointArray]) -> cpy.PointArray:
|
| 108 |
+
"""Concatenate a list of point arrays into a single point array.
|
| 109 |
+
"""
|
| 110 |
+
if not list_of_points:
|
| 111 |
+
raise ValueError("Empty list passed to concat_points")
|
| 112 |
+
|
| 113 |
+
return np.concatenate(list_of_points, dtype=point_dtype)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def concat_points_or_none(
|
| 117 |
+
list_of_points_or_none: list[cpy.PointArray | None],
|
| 118 |
+
) -> cpy.PointArray | None:
|
| 119 |
+
"""Concatenate a list of point arrays or None into a single point array or None.
|
| 120 |
+
"""
|
| 121 |
+
list_of_points = [points for points in list_of_points_or_none if points is not None]
|
| 122 |
+
if list_of_points:
|
| 123 |
+
return concat_points(list_of_points)
|
| 124 |
+
else:
|
| 125 |
+
return None
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def concat_points_or_none_with_nan(
|
| 129 |
+
list_of_points_or_none: list[cpy.PointArray | None],
|
| 130 |
+
) -> cpy.PointArray | None:
|
| 131 |
+
"""Concatenate a list of points or None into a single point array or None, with NaNs used to
|
| 132 |
+
separate each line.
|
| 133 |
+
"""
|
| 134 |
+
list_of_points = [points for points in list_of_points_or_none if points is not None]
|
| 135 |
+
if list_of_points:
|
| 136 |
+
return concat_points_with_nan(list_of_points)
|
| 137 |
+
else:
|
| 138 |
+
return None
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def concat_points_with_nan(list_of_points: list[cpy.PointArray]) -> cpy.PointArray:
|
| 142 |
+
"""Concatenate a list of points into a single point array with NaNs used to separate each line.
|
| 143 |
+
"""
|
| 144 |
+
if not list_of_points:
|
| 145 |
+
raise ValueError("Empty list passed to concat_points_with_nan")
|
| 146 |
+
|
| 147 |
+
if len(list_of_points) == 1:
|
| 148 |
+
return list_of_points[0]
|
| 149 |
+
else:
|
| 150 |
+
nan_spacer = np.full((1, 2), np.nan, dtype=point_dtype)
|
| 151 |
+
list_of_points = [list_of_points[0],
|
| 152 |
+
*list(chain(*((nan_spacer, x) for x in list_of_points[1:])))]
|
| 153 |
+
return concat_points(list_of_points)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def insert_nan_at_offsets(points: cpy.PointArray, offsets: cpy.OffsetArray) -> cpy.PointArray:
|
| 157 |
+
"""Insert NaNs into a point array at locations specified by an offset array.
|
| 158 |
+
"""
|
| 159 |
+
check_point_array(points)
|
| 160 |
+
check_offset_array(offsets)
|
| 161 |
+
|
| 162 |
+
if len(offsets) <= 2:
|
| 163 |
+
return points
|
| 164 |
+
else:
|
| 165 |
+
nan_spacer = np.array([np.nan, np.nan], dtype=point_dtype)
|
| 166 |
+
# Convert offsets to int64 to avoid numpy error when mixing signed and unsigned ints.
|
| 167 |
+
return np.insert(points, offsets[1:-1].astype(np.int64), nan_spacer, axis=0)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def offsets_from_codes(codes: cpy.CodeArray) -> cpy.OffsetArray:
|
| 171 |
+
"""Determine offsets from codes using locations of MOVETO codes.
|
| 172 |
+
"""
|
| 173 |
+
check_code_array(codes)
|
| 174 |
+
|
| 175 |
+
return np.append(np.nonzero(codes == MOVETO)[0], len(codes)).astype(offset_dtype)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def offsets_from_lengths(list_of_points: list[cpy.PointArray]) -> cpy.OffsetArray:
|
| 179 |
+
"""Determine offsets from lengths of point arrays.
|
| 180 |
+
"""
|
| 181 |
+
if not list_of_points:
|
| 182 |
+
raise ValueError("Empty list passed to offsets_from_lengths")
|
| 183 |
+
|
| 184 |
+
return np.cumsum([0] + [len(line) for line in list_of_points], dtype=offset_dtype)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def outer_offsets_from_list_of_codes(list_of_codes: list[cpy.CodeArray]) -> cpy.OffsetArray:
|
| 188 |
+
"""Determine outer offsets from codes using locations of MOVETO codes.
|
| 189 |
+
"""
|
| 190 |
+
if not list_of_codes:
|
| 191 |
+
raise ValueError("Empty list passed to outer_offsets_from_list_of_codes")
|
| 192 |
+
|
| 193 |
+
return np.cumsum([0] + [np.count_nonzero(codes == MOVETO) for codes in list_of_codes],
|
| 194 |
+
dtype=offset_dtype)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def outer_offsets_from_list_of_offsets(list_of_offsets: list[cpy.OffsetArray]) -> cpy.OffsetArray:
|
| 198 |
+
"""Determine outer offsets from a list of offsets.
|
| 199 |
+
"""
|
| 200 |
+
if not list_of_offsets:
|
| 201 |
+
raise ValueError("Empty list passed to outer_offsets_from_list_of_offsets")
|
| 202 |
+
|
| 203 |
+
return np.cumsum([0] + [len(offsets)-1 for offsets in list_of_offsets], dtype=offset_dtype)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def remove_nan(points: cpy.PointArray) -> tuple[cpy.PointArray, cpy.OffsetArray]:
|
| 207 |
+
"""Remove NaN from a points array, also return the offsets corresponding to the NaN removed.
|
| 208 |
+
"""
|
| 209 |
+
check_point_array(points)
|
| 210 |
+
|
| 211 |
+
nan_offsets = np.nonzero(np.isnan(points[:, 0]))[0]
|
| 212 |
+
if len(nan_offsets) == 0:
|
| 213 |
+
return points, np.array([0, len(points)], dtype=offset_dtype)
|
| 214 |
+
else:
|
| 215 |
+
points = np.delete(points, nan_offsets, axis=0)
|
| 216 |
+
nan_offsets -= np.arange(len(nan_offsets))
|
| 217 |
+
offsets: cpy.OffsetArray = np.empty(len(nan_offsets)+2, dtype=offset_dtype)
|
| 218 |
+
offsets[0] = 0
|
| 219 |
+
offsets[1:-1] = nan_offsets
|
| 220 |
+
offsets[-1] = len(points)
|
| 221 |
+
return points, offsets
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def split_codes_by_offsets(codes: cpy.CodeArray, offsets: cpy.OffsetArray) -> list[cpy.CodeArray]:
|
| 225 |
+
"""Split a code array at locations specified by an offset array into a list of code arrays.
|
| 226 |
+
"""
|
| 227 |
+
check_code_array(codes)
|
| 228 |
+
check_offset_array(offsets)
|
| 229 |
+
|
| 230 |
+
if len(offsets) > 2:
|
| 231 |
+
return np.split(codes, offsets[1:-1])
|
| 232 |
+
else:
|
| 233 |
+
return [codes]
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def split_points_by_offsets(
|
| 237 |
+
points: cpy.PointArray,
|
| 238 |
+
offsets: cpy.OffsetArray,
|
| 239 |
+
) -> list[cpy.PointArray]:
|
| 240 |
+
"""Split a point array at locations specified by an offset array into a list of point arrays.
|
| 241 |
+
"""
|
| 242 |
+
check_point_array(points)
|
| 243 |
+
check_offset_array(offsets)
|
| 244 |
+
|
| 245 |
+
if len(offsets) > 2:
|
| 246 |
+
return np.split(points, offsets[1:-1])
|
| 247 |
+
else:
|
| 248 |
+
return [points]
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def split_points_at_nan(points: cpy.PointArray) -> list[cpy.PointArray]:
|
| 252 |
+
"""Split a points array at NaNs into a list of point arrays.
|
| 253 |
+
"""
|
| 254 |
+
check_point_array(points)
|
| 255 |
+
|
| 256 |
+
nan_offsets = np.nonzero(np.isnan(points[:, 0]))[0]
|
| 257 |
+
if len(nan_offsets) == 0:
|
| 258 |
+
return [points]
|
| 259 |
+
else:
|
| 260 |
+
nan_offsets = np.concatenate(([-1], nan_offsets, [len(points)]))
|
| 261 |
+
return [points[s+1:e] for s, e in pairwise(nan_offsets)]
|
venv/lib/python3.10/site-packages/contourpy/chunk.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def calc_chunk_sizes(
|
| 7 |
+
chunk_size: int | tuple[int, int] | None,
|
| 8 |
+
chunk_count: int | tuple[int, int] | None,
|
| 9 |
+
total_chunk_count: int | None,
|
| 10 |
+
ny: int,
|
| 11 |
+
nx: int,
|
| 12 |
+
) -> tuple[int, int]:
|
| 13 |
+
"""Calculate chunk sizes.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
chunk_size (int or tuple(int, int), optional): Chunk size in (y, x) directions, or the same
|
| 17 |
+
size in both directions if only one is specified. Cannot be negative.
|
| 18 |
+
chunk_count (int or tuple(int, int), optional): Chunk count in (y, x) directions, or the
|
| 19 |
+
same count in both directions if only one is specified. If less than 1, set to 1.
|
| 20 |
+
total_chunk_count (int, optional): Total number of chunks. If less than 1, set to 1.
|
| 21 |
+
ny (int): Number of grid points in y-direction.
|
| 22 |
+
nx (int): Number of grid points in x-direction.
|
| 23 |
+
|
| 24 |
+
Return:
|
| 25 |
+
tuple(int, int): Chunk sizes (y_chunk_size, x_chunk_size).
|
| 26 |
+
|
| 27 |
+
Note:
|
| 28 |
+
Zero or one of ``chunk_size``, ``chunk_count`` and ``total_chunk_count`` should be
|
| 29 |
+
specified.
|
| 30 |
+
"""
|
| 31 |
+
if sum([chunk_size is not None, chunk_count is not None, total_chunk_count is not None]) > 1:
|
| 32 |
+
raise ValueError("Only one of chunk_size, chunk_count and total_chunk_count should be set")
|
| 33 |
+
|
| 34 |
+
if nx < 2 or ny < 2:
|
| 35 |
+
raise ValueError(f"(ny, nx) must be at least (2, 2), not ({ny}, {nx})")
|
| 36 |
+
|
| 37 |
+
if total_chunk_count is not None:
|
| 38 |
+
max_chunk_count = (nx-1)*(ny-1)
|
| 39 |
+
total_chunk_count = min(max(total_chunk_count, 1), max_chunk_count)
|
| 40 |
+
if total_chunk_count == 1:
|
| 41 |
+
chunk_size = 0
|
| 42 |
+
elif total_chunk_count == max_chunk_count:
|
| 43 |
+
chunk_size = (1, 1)
|
| 44 |
+
else:
|
| 45 |
+
factors = two_factors(total_chunk_count)
|
| 46 |
+
if ny > nx:
|
| 47 |
+
chunk_count = factors
|
| 48 |
+
else:
|
| 49 |
+
chunk_count = (factors[1], factors[0])
|
| 50 |
+
|
| 51 |
+
if chunk_count is not None:
|
| 52 |
+
if isinstance(chunk_count, tuple):
|
| 53 |
+
y_chunk_count, x_chunk_count = chunk_count
|
| 54 |
+
else:
|
| 55 |
+
y_chunk_count = x_chunk_count = chunk_count
|
| 56 |
+
x_chunk_count = min(max(x_chunk_count, 1), nx-1)
|
| 57 |
+
y_chunk_count = min(max(y_chunk_count, 1), ny-1)
|
| 58 |
+
chunk_size = (math.ceil((ny-1) / y_chunk_count), math.ceil((nx-1) / x_chunk_count))
|
| 59 |
+
|
| 60 |
+
if chunk_size is None:
|
| 61 |
+
y_chunk_size = x_chunk_size = 0
|
| 62 |
+
elif isinstance(chunk_size, tuple):
|
| 63 |
+
y_chunk_size, x_chunk_size = chunk_size
|
| 64 |
+
else:
|
| 65 |
+
y_chunk_size = x_chunk_size = chunk_size
|
| 66 |
+
|
| 67 |
+
if x_chunk_size < 0 or y_chunk_size < 0:
|
| 68 |
+
raise ValueError("chunk_size cannot be negative")
|
| 69 |
+
|
| 70 |
+
return y_chunk_size, x_chunk_size
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def two_factors(n: int) -> tuple[int, int]:
|
| 74 |
+
"""Split an integer into two integer factors.
|
| 75 |
+
|
| 76 |
+
The two factors will be as close as possible to the sqrt of n, and are returned in decreasing
|
| 77 |
+
order. Worst case returns (n, 1).
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
n (int): The integer to factorize, must be positive.
|
| 81 |
+
|
| 82 |
+
Return:
|
| 83 |
+
tuple(int, int): The two factors of n, in decreasing order.
|
| 84 |
+
"""
|
| 85 |
+
if n < 0:
|
| 86 |
+
raise ValueError(f"two_factors expects positive integer not {n}")
|
| 87 |
+
|
| 88 |
+
i = math.ceil(math.sqrt(n))
|
| 89 |
+
while n % i != 0:
|
| 90 |
+
i -= 1
|
| 91 |
+
j = n // i
|
| 92 |
+
if i > j:
|
| 93 |
+
return i, j
|
| 94 |
+
else:
|
| 95 |
+
return j, i
|
venv/lib/python3.10/site-packages/contourpy/convert.py
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from itertools import pairwise
|
| 4 |
+
from typing import TYPE_CHECKING, cast
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
from contourpy._contourpy import FillType, LineType
|
| 9 |
+
import contourpy.array as arr
|
| 10 |
+
from contourpy.enum_util import as_fill_type, as_line_type
|
| 11 |
+
from contourpy.typecheck import check_filled, check_lines
|
| 12 |
+
from contourpy.types import MOVETO, offset_dtype
|
| 13 |
+
|
| 14 |
+
if TYPE_CHECKING:
|
| 15 |
+
import contourpy._contourpy as cpy
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _convert_filled_from_OuterCode(
|
| 19 |
+
filled: cpy.FillReturn_OuterCode,
|
| 20 |
+
fill_type_to: FillType,
|
| 21 |
+
) -> cpy.FillReturn:
|
| 22 |
+
if fill_type_to == FillType.OuterCode:
|
| 23 |
+
return filled
|
| 24 |
+
elif fill_type_to == FillType.OuterOffset:
|
| 25 |
+
return (filled[0], [arr.offsets_from_codes(codes) for codes in filled[1]])
|
| 26 |
+
|
| 27 |
+
if len(filled[0]) > 0:
|
| 28 |
+
points = arr.concat_points(filled[0])
|
| 29 |
+
codes = arr.concat_codes(filled[1])
|
| 30 |
+
else:
|
| 31 |
+
points = None
|
| 32 |
+
codes = None
|
| 33 |
+
|
| 34 |
+
if fill_type_to == FillType.ChunkCombinedCode:
|
| 35 |
+
return ([points], [codes])
|
| 36 |
+
elif fill_type_to == FillType.ChunkCombinedOffset:
|
| 37 |
+
return ([points], [None if codes is None else arr.offsets_from_codes(codes)])
|
| 38 |
+
elif fill_type_to == FillType.ChunkCombinedCodeOffset:
|
| 39 |
+
outer_offsets = None if points is None else arr.offsets_from_lengths(filled[0])
|
| 40 |
+
ret1: cpy.FillReturn_ChunkCombinedCodeOffset = ([points], [codes], [outer_offsets])
|
| 41 |
+
return ret1
|
| 42 |
+
elif fill_type_to == FillType.ChunkCombinedOffsetOffset:
|
| 43 |
+
if codes is None:
|
| 44 |
+
ret2: cpy.FillReturn_ChunkCombinedOffsetOffset = ([None], [None], [None])
|
| 45 |
+
else:
|
| 46 |
+
offsets = arr.offsets_from_codes(codes)
|
| 47 |
+
outer_offsets = arr.outer_offsets_from_list_of_codes(filled[1])
|
| 48 |
+
ret2 = ([points], [offsets], [outer_offsets])
|
| 49 |
+
return ret2
|
| 50 |
+
else:
|
| 51 |
+
raise ValueError(f"Invalid FillType {fill_type_to}")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _convert_filled_from_OuterOffset(
|
| 55 |
+
filled: cpy.FillReturn_OuterOffset,
|
| 56 |
+
fill_type_to: FillType,
|
| 57 |
+
) -> cpy.FillReturn:
|
| 58 |
+
if fill_type_to == FillType.OuterCode:
|
| 59 |
+
separate_codes = [arr.codes_from_offsets(offsets) for offsets in filled[1]]
|
| 60 |
+
return (filled[0], separate_codes)
|
| 61 |
+
elif fill_type_to == FillType.OuterOffset:
|
| 62 |
+
return filled
|
| 63 |
+
|
| 64 |
+
if len(filled[0]) > 0:
|
| 65 |
+
points = arr.concat_points(filled[0])
|
| 66 |
+
offsets = arr.concat_offsets(filled[1])
|
| 67 |
+
else:
|
| 68 |
+
points = None
|
| 69 |
+
offsets = None
|
| 70 |
+
|
| 71 |
+
if fill_type_to == FillType.ChunkCombinedCode:
|
| 72 |
+
return ([points], [None if offsets is None else arr.codes_from_offsets(offsets)])
|
| 73 |
+
elif fill_type_to == FillType.ChunkCombinedOffset:
|
| 74 |
+
return ([points], [offsets])
|
| 75 |
+
elif fill_type_to == FillType.ChunkCombinedCodeOffset:
|
| 76 |
+
if offsets is None:
|
| 77 |
+
ret1: cpy.FillReturn_ChunkCombinedCodeOffset = ([None], [None], [None])
|
| 78 |
+
else:
|
| 79 |
+
codes = arr.codes_from_offsets(offsets)
|
| 80 |
+
outer_offsets = arr.offsets_from_lengths(filled[0])
|
| 81 |
+
ret1 = ([points], [codes], [outer_offsets])
|
| 82 |
+
return ret1
|
| 83 |
+
elif fill_type_to == FillType.ChunkCombinedOffsetOffset:
|
| 84 |
+
if points is None:
|
| 85 |
+
ret2: cpy.FillReturn_ChunkCombinedOffsetOffset = ([None], [None], [None])
|
| 86 |
+
else:
|
| 87 |
+
outer_offsets = arr.outer_offsets_from_list_of_offsets(filled[1])
|
| 88 |
+
ret2 = ([points], [offsets], [outer_offsets])
|
| 89 |
+
return ret2
|
| 90 |
+
else:
|
| 91 |
+
raise ValueError(f"Invalid FillType {fill_type_to}")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _convert_filled_from_ChunkCombinedCode(
|
| 95 |
+
filled: cpy.FillReturn_ChunkCombinedCode,
|
| 96 |
+
fill_type_to: FillType,
|
| 97 |
+
) -> cpy.FillReturn:
|
| 98 |
+
if fill_type_to == FillType.ChunkCombinedCode:
|
| 99 |
+
return filled
|
| 100 |
+
elif fill_type_to == FillType.ChunkCombinedOffset:
|
| 101 |
+
codes = [None if codes is None else arr.offsets_from_codes(codes) for codes in filled[1]]
|
| 102 |
+
return (filled[0], codes)
|
| 103 |
+
else:
|
| 104 |
+
raise ValueError(
|
| 105 |
+
f"Conversion from {FillType.ChunkCombinedCode} to {fill_type_to} not supported")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _convert_filled_from_ChunkCombinedOffset(
|
| 109 |
+
filled: cpy.FillReturn_ChunkCombinedOffset,
|
| 110 |
+
fill_type_to: FillType,
|
| 111 |
+
) -> cpy.FillReturn:
|
| 112 |
+
if fill_type_to == FillType.ChunkCombinedCode:
|
| 113 |
+
chunk_codes: list[cpy.CodeArray | None] = []
|
| 114 |
+
for points, offsets in zip(*filled):
|
| 115 |
+
if points is None:
|
| 116 |
+
chunk_codes.append(None)
|
| 117 |
+
else:
|
| 118 |
+
if TYPE_CHECKING:
|
| 119 |
+
assert offsets is not None
|
| 120 |
+
chunk_codes.append(arr.codes_from_offsets_and_points(offsets, points))
|
| 121 |
+
return (filled[0], chunk_codes)
|
| 122 |
+
elif fill_type_to == FillType.ChunkCombinedOffset:
|
| 123 |
+
return filled
|
| 124 |
+
else:
|
| 125 |
+
raise ValueError(
|
| 126 |
+
f"Conversion from {FillType.ChunkCombinedOffset} to {fill_type_to} not supported")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _convert_filled_from_ChunkCombinedCodeOffset(
|
| 130 |
+
filled: cpy.FillReturn_ChunkCombinedCodeOffset,
|
| 131 |
+
fill_type_to: FillType,
|
| 132 |
+
) -> cpy.FillReturn:
|
| 133 |
+
if fill_type_to == FillType.OuterCode:
|
| 134 |
+
separate_points = []
|
| 135 |
+
separate_codes = []
|
| 136 |
+
for points, codes, outer_offsets in zip(*filled):
|
| 137 |
+
if points is not None:
|
| 138 |
+
if TYPE_CHECKING:
|
| 139 |
+
assert codes is not None
|
| 140 |
+
assert outer_offsets is not None
|
| 141 |
+
separate_points += arr.split_points_by_offsets(points, outer_offsets)
|
| 142 |
+
separate_codes += arr.split_codes_by_offsets(codes, outer_offsets)
|
| 143 |
+
return (separate_points, separate_codes)
|
| 144 |
+
elif fill_type_to == FillType.OuterOffset:
|
| 145 |
+
separate_points = []
|
| 146 |
+
separate_offsets = []
|
| 147 |
+
for points, codes, outer_offsets in zip(*filled):
|
| 148 |
+
if points is not None:
|
| 149 |
+
if TYPE_CHECKING:
|
| 150 |
+
assert codes is not None
|
| 151 |
+
assert outer_offsets is not None
|
| 152 |
+
separate_points += arr.split_points_by_offsets(points, outer_offsets)
|
| 153 |
+
separate_codes = arr.split_codes_by_offsets(codes, outer_offsets)
|
| 154 |
+
separate_offsets += [arr.offsets_from_codes(codes) for codes in separate_codes]
|
| 155 |
+
return (separate_points, separate_offsets)
|
| 156 |
+
elif fill_type_to == FillType.ChunkCombinedCode:
|
| 157 |
+
ret1: cpy.FillReturn_ChunkCombinedCode = (filled[0], filled[1])
|
| 158 |
+
return ret1
|
| 159 |
+
elif fill_type_to == FillType.ChunkCombinedOffset:
|
| 160 |
+
all_offsets = [None if codes is None else arr.offsets_from_codes(codes)
|
| 161 |
+
for codes in filled[1]]
|
| 162 |
+
ret2: cpy.FillReturn_ChunkCombinedOffset = (filled[0], all_offsets)
|
| 163 |
+
return ret2
|
| 164 |
+
elif fill_type_to == FillType.ChunkCombinedCodeOffset:
|
| 165 |
+
return filled
|
| 166 |
+
elif fill_type_to == FillType.ChunkCombinedOffsetOffset:
|
| 167 |
+
chunk_offsets: list[cpy.OffsetArray | None] = []
|
| 168 |
+
chunk_outer_offsets: list[cpy.OffsetArray | None] = []
|
| 169 |
+
for codes, outer_offsets in zip(*filled[1:]):
|
| 170 |
+
if codes is None:
|
| 171 |
+
chunk_offsets.append(None)
|
| 172 |
+
chunk_outer_offsets.append(None)
|
| 173 |
+
else:
|
| 174 |
+
if TYPE_CHECKING:
|
| 175 |
+
assert outer_offsets is not None
|
| 176 |
+
offsets = arr.offsets_from_codes(codes)
|
| 177 |
+
outer_offsets = np.array([np.nonzero(offsets == oo)[0][0] for oo in outer_offsets],
|
| 178 |
+
dtype=offset_dtype)
|
| 179 |
+
chunk_offsets.append(offsets)
|
| 180 |
+
chunk_outer_offsets.append(outer_offsets)
|
| 181 |
+
ret3: cpy.FillReturn_ChunkCombinedOffsetOffset = (
|
| 182 |
+
filled[0], chunk_offsets, chunk_outer_offsets,
|
| 183 |
+
)
|
| 184 |
+
return ret3
|
| 185 |
+
else:
|
| 186 |
+
raise ValueError(f"Invalid FillType {fill_type_to}")
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _convert_filled_from_ChunkCombinedOffsetOffset(
|
| 190 |
+
filled: cpy.FillReturn_ChunkCombinedOffsetOffset,
|
| 191 |
+
fill_type_to: FillType,
|
| 192 |
+
) -> cpy.FillReturn:
|
| 193 |
+
if fill_type_to == FillType.OuterCode:
|
| 194 |
+
separate_points = []
|
| 195 |
+
separate_codes = []
|
| 196 |
+
for points, offsets, outer_offsets in zip(*filled):
|
| 197 |
+
if points is not None:
|
| 198 |
+
if TYPE_CHECKING:
|
| 199 |
+
assert offsets is not None
|
| 200 |
+
assert outer_offsets is not None
|
| 201 |
+
codes = arr.codes_from_offsets_and_points(offsets, points)
|
| 202 |
+
outer_offsets = offsets[outer_offsets]
|
| 203 |
+
separate_points += arr.split_points_by_offsets(points, outer_offsets)
|
| 204 |
+
separate_codes += arr.split_codes_by_offsets(codes, outer_offsets)
|
| 205 |
+
return (separate_points, separate_codes)
|
| 206 |
+
elif fill_type_to == FillType.OuterOffset:
|
| 207 |
+
separate_points = []
|
| 208 |
+
separate_offsets = []
|
| 209 |
+
for points, offsets, outer_offsets in zip(*filled):
|
| 210 |
+
if points is not None:
|
| 211 |
+
if TYPE_CHECKING:
|
| 212 |
+
assert offsets is not None
|
| 213 |
+
assert outer_offsets is not None
|
| 214 |
+
if len(outer_offsets) > 2:
|
| 215 |
+
separate_offsets += [offsets[s:e+1] - offsets[s] for s, e in
|
| 216 |
+
pairwise(outer_offsets)]
|
| 217 |
+
else:
|
| 218 |
+
separate_offsets.append(offsets)
|
| 219 |
+
separate_points += arr.split_points_by_offsets(points, offsets[outer_offsets])
|
| 220 |
+
return (separate_points, separate_offsets)
|
| 221 |
+
elif fill_type_to == FillType.ChunkCombinedCode:
|
| 222 |
+
chunk_codes: list[cpy.CodeArray | None] = []
|
| 223 |
+
for points, offsets, outer_offsets in zip(*filled):
|
| 224 |
+
if points is None:
|
| 225 |
+
chunk_codes.append(None)
|
| 226 |
+
else:
|
| 227 |
+
if TYPE_CHECKING:
|
| 228 |
+
assert offsets is not None
|
| 229 |
+
assert outer_offsets is not None
|
| 230 |
+
chunk_codes.append(arr.codes_from_offsets_and_points(offsets, points))
|
| 231 |
+
ret1: cpy.FillReturn_ChunkCombinedCode = (filled[0], chunk_codes)
|
| 232 |
+
return ret1
|
| 233 |
+
elif fill_type_to == FillType.ChunkCombinedOffset:
|
| 234 |
+
return (filled[0], filled[1])
|
| 235 |
+
elif fill_type_to == FillType.ChunkCombinedCodeOffset:
|
| 236 |
+
chunk_codes = []
|
| 237 |
+
chunk_outer_offsets: list[cpy.OffsetArray | None] = []
|
| 238 |
+
for points, offsets, outer_offsets in zip(*filled):
|
| 239 |
+
if points is None:
|
| 240 |
+
chunk_codes.append(None)
|
| 241 |
+
chunk_outer_offsets.append(None)
|
| 242 |
+
else:
|
| 243 |
+
if TYPE_CHECKING:
|
| 244 |
+
assert offsets is not None
|
| 245 |
+
assert outer_offsets is not None
|
| 246 |
+
chunk_codes.append(arr.codes_from_offsets_and_points(offsets, points))
|
| 247 |
+
chunk_outer_offsets.append(offsets[outer_offsets])
|
| 248 |
+
ret2: cpy.FillReturn_ChunkCombinedCodeOffset = (filled[0], chunk_codes, chunk_outer_offsets)
|
| 249 |
+
return ret2
|
| 250 |
+
elif fill_type_to == FillType.ChunkCombinedOffsetOffset:
|
| 251 |
+
return filled
|
| 252 |
+
else:
|
| 253 |
+
raise ValueError(f"Invalid FillType {fill_type_to}")
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def convert_filled(
|
| 257 |
+
filled: cpy.FillReturn,
|
| 258 |
+
fill_type_from: FillType | str,
|
| 259 |
+
fill_type_to: FillType | str,
|
| 260 |
+
) -> cpy.FillReturn:
|
| 261 |
+
"""Convert filled contours from one :class:`~.FillType` to another.
|
| 262 |
+
|
| 263 |
+
Args:
|
| 264 |
+
filled (sequence of arrays): Filled contour polygons to convert, such as those returned by
|
| 265 |
+
:meth:`.ContourGenerator.filled`.
|
| 266 |
+
fill_type_from (FillType or str): :class:`~.FillType` to convert from as enum or
|
| 267 |
+
string equivalent.
|
| 268 |
+
fill_type_to (FillType or str): :class:`~.FillType` to convert to as enum or string
|
| 269 |
+
equivalent.
|
| 270 |
+
|
| 271 |
+
Return:
|
| 272 |
+
Converted filled contour polygons.
|
| 273 |
+
|
| 274 |
+
When converting non-chunked fill types (``FillType.OuterCode`` or ``FillType.OuterOffset``) to
|
| 275 |
+
chunked ones, all polygons are placed in the first chunk. When converting in the other
|
| 276 |
+
direction, all chunk information is discarded. Converting a fill type that is not aware of the
|
| 277 |
+
relationship between outer boundaries and contained holes (``FillType.ChunkCombinedCode`` or
|
| 278 |
+
``FillType.ChunkCombinedOffset``) to one that is will raise a ``ValueError``.
|
| 279 |
+
|
| 280 |
+
.. versionadded:: 1.2.0
|
| 281 |
+
"""
|
| 282 |
+
fill_type_from = as_fill_type(fill_type_from)
|
| 283 |
+
fill_type_to = as_fill_type(fill_type_to)
|
| 284 |
+
|
| 285 |
+
check_filled(filled, fill_type_from)
|
| 286 |
+
|
| 287 |
+
if fill_type_from == FillType.OuterCode:
|
| 288 |
+
if TYPE_CHECKING:
|
| 289 |
+
filled = cast(cpy.FillReturn_OuterCode, filled)
|
| 290 |
+
return _convert_filled_from_OuterCode(filled, fill_type_to)
|
| 291 |
+
elif fill_type_from == FillType.OuterOffset:
|
| 292 |
+
if TYPE_CHECKING:
|
| 293 |
+
filled = cast(cpy.FillReturn_OuterOffset, filled)
|
| 294 |
+
return _convert_filled_from_OuterOffset(filled, fill_type_to)
|
| 295 |
+
elif fill_type_from == FillType.ChunkCombinedCode:
|
| 296 |
+
if TYPE_CHECKING:
|
| 297 |
+
filled = cast(cpy.FillReturn_ChunkCombinedCode, filled)
|
| 298 |
+
return _convert_filled_from_ChunkCombinedCode(filled, fill_type_to)
|
| 299 |
+
elif fill_type_from == FillType.ChunkCombinedOffset:
|
| 300 |
+
if TYPE_CHECKING:
|
| 301 |
+
filled = cast(cpy.FillReturn_ChunkCombinedOffset, filled)
|
| 302 |
+
return _convert_filled_from_ChunkCombinedOffset(filled, fill_type_to)
|
| 303 |
+
elif fill_type_from == FillType.ChunkCombinedCodeOffset:
|
| 304 |
+
if TYPE_CHECKING:
|
| 305 |
+
filled = cast(cpy.FillReturn_ChunkCombinedCodeOffset, filled)
|
| 306 |
+
return _convert_filled_from_ChunkCombinedCodeOffset(filled, fill_type_to)
|
| 307 |
+
elif fill_type_from == FillType.ChunkCombinedOffsetOffset:
|
| 308 |
+
if TYPE_CHECKING:
|
| 309 |
+
filled = cast(cpy.FillReturn_ChunkCombinedOffsetOffset, filled)
|
| 310 |
+
return _convert_filled_from_ChunkCombinedOffsetOffset(filled, fill_type_to)
|
| 311 |
+
else:
|
| 312 |
+
raise ValueError(f"Invalid FillType {fill_type_from}")
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def _convert_lines_from_Separate(
|
| 316 |
+
lines: cpy.LineReturn_Separate,
|
| 317 |
+
line_type_to: LineType,
|
| 318 |
+
) -> cpy.LineReturn:
|
| 319 |
+
if line_type_to == LineType.Separate:
|
| 320 |
+
return lines
|
| 321 |
+
elif line_type_to == LineType.SeparateCode:
|
| 322 |
+
separate_codes = [arr.codes_from_points(line) for line in lines]
|
| 323 |
+
return (lines, separate_codes)
|
| 324 |
+
elif line_type_to == LineType.ChunkCombinedCode:
|
| 325 |
+
if not lines:
|
| 326 |
+
ret1: cpy.LineReturn_ChunkCombinedCode = ([None], [None])
|
| 327 |
+
else:
|
| 328 |
+
points = arr.concat_points(lines)
|
| 329 |
+
offsets = arr.offsets_from_lengths(lines)
|
| 330 |
+
codes = arr.codes_from_offsets_and_points(offsets, points)
|
| 331 |
+
ret1 = ([points], [codes])
|
| 332 |
+
return ret1
|
| 333 |
+
elif line_type_to == LineType.ChunkCombinedOffset:
|
| 334 |
+
if not lines:
|
| 335 |
+
ret2: cpy.LineReturn_ChunkCombinedOffset = ([None], [None])
|
| 336 |
+
else:
|
| 337 |
+
ret2 = ([arr.concat_points(lines)], [arr.offsets_from_lengths(lines)])
|
| 338 |
+
return ret2
|
| 339 |
+
elif line_type_to == LineType.ChunkCombinedNan:
|
| 340 |
+
if not lines:
|
| 341 |
+
ret3: cpy.LineReturn_ChunkCombinedNan = ([None],)
|
| 342 |
+
else:
|
| 343 |
+
ret3 = ([arr.concat_points_with_nan(lines)],)
|
| 344 |
+
return ret3
|
| 345 |
+
else:
|
| 346 |
+
raise ValueError(f"Invalid LineType {line_type_to}")
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def _convert_lines_from_SeparateCode(
|
| 350 |
+
lines: cpy.LineReturn_SeparateCode,
|
| 351 |
+
line_type_to: LineType,
|
| 352 |
+
) -> cpy.LineReturn:
|
| 353 |
+
if line_type_to == LineType.Separate:
|
| 354 |
+
# Drop codes.
|
| 355 |
+
return lines[0]
|
| 356 |
+
elif line_type_to == LineType.SeparateCode:
|
| 357 |
+
return lines
|
| 358 |
+
elif line_type_to == LineType.ChunkCombinedCode:
|
| 359 |
+
if not lines[0]:
|
| 360 |
+
ret1: cpy.LineReturn_ChunkCombinedCode = ([None], [None])
|
| 361 |
+
else:
|
| 362 |
+
ret1 = ([arr.concat_points(lines[0])], [arr.concat_codes(lines[1])])
|
| 363 |
+
return ret1
|
| 364 |
+
elif line_type_to == LineType.ChunkCombinedOffset:
|
| 365 |
+
if not lines[0]:
|
| 366 |
+
ret2: cpy.LineReturn_ChunkCombinedOffset = ([None], [None])
|
| 367 |
+
else:
|
| 368 |
+
ret2 = ([arr.concat_points(lines[0])], [arr.offsets_from_lengths(lines[0])])
|
| 369 |
+
return ret2
|
| 370 |
+
elif line_type_to == LineType.ChunkCombinedNan:
|
| 371 |
+
if not lines[0]:
|
| 372 |
+
ret3: cpy.LineReturn_ChunkCombinedNan = ([None],)
|
| 373 |
+
else:
|
| 374 |
+
ret3 = ([arr.concat_points_with_nan(lines[0])],)
|
| 375 |
+
return ret3
|
| 376 |
+
else:
|
| 377 |
+
raise ValueError(f"Invalid LineType {line_type_to}")
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def _convert_lines_from_ChunkCombinedCode(
|
| 381 |
+
lines: cpy.LineReturn_ChunkCombinedCode,
|
| 382 |
+
line_type_to: LineType,
|
| 383 |
+
) -> cpy.LineReturn:
|
| 384 |
+
if line_type_to in (LineType.Separate, LineType.SeparateCode):
|
| 385 |
+
separate_lines = []
|
| 386 |
+
for points, codes in zip(*lines):
|
| 387 |
+
if points is not None:
|
| 388 |
+
if TYPE_CHECKING:
|
| 389 |
+
assert codes is not None
|
| 390 |
+
split_at = np.nonzero(codes == MOVETO)[0]
|
| 391 |
+
if len(split_at) > 1:
|
| 392 |
+
separate_lines += np.split(points, split_at[1:])
|
| 393 |
+
else:
|
| 394 |
+
separate_lines.append(points)
|
| 395 |
+
if line_type_to == LineType.Separate:
|
| 396 |
+
return separate_lines
|
| 397 |
+
else:
|
| 398 |
+
separate_codes = [arr.codes_from_points(line) for line in separate_lines]
|
| 399 |
+
return (separate_lines, separate_codes)
|
| 400 |
+
elif line_type_to == LineType.ChunkCombinedCode:
|
| 401 |
+
return lines
|
| 402 |
+
elif line_type_to == LineType.ChunkCombinedOffset:
|
| 403 |
+
chunk_offsets = [None if codes is None else arr.offsets_from_codes(codes)
|
| 404 |
+
for codes in lines[1]]
|
| 405 |
+
return (lines[0], chunk_offsets)
|
| 406 |
+
elif line_type_to == LineType.ChunkCombinedNan:
|
| 407 |
+
points_nan: list[cpy.PointArray | None] = []
|
| 408 |
+
for points, codes in zip(*lines):
|
| 409 |
+
if points is None:
|
| 410 |
+
points_nan.append(None)
|
| 411 |
+
else:
|
| 412 |
+
if TYPE_CHECKING:
|
| 413 |
+
assert codes is not None
|
| 414 |
+
offsets = arr.offsets_from_codes(codes)
|
| 415 |
+
points_nan.append(arr.insert_nan_at_offsets(points, offsets))
|
| 416 |
+
return (points_nan,)
|
| 417 |
+
else:
|
| 418 |
+
raise ValueError(f"Invalid LineType {line_type_to}")
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def _convert_lines_from_ChunkCombinedOffset(
|
| 422 |
+
lines: cpy.LineReturn_ChunkCombinedOffset,
|
| 423 |
+
line_type_to: LineType,
|
| 424 |
+
) -> cpy.LineReturn:
|
| 425 |
+
if line_type_to in (LineType.Separate, LineType.SeparateCode):
|
| 426 |
+
separate_lines = []
|
| 427 |
+
for points, offsets in zip(*lines):
|
| 428 |
+
if points is not None:
|
| 429 |
+
if TYPE_CHECKING:
|
| 430 |
+
assert offsets is not None
|
| 431 |
+
separate_lines += arr.split_points_by_offsets(points, offsets)
|
| 432 |
+
if line_type_to == LineType.Separate:
|
| 433 |
+
return separate_lines
|
| 434 |
+
else:
|
| 435 |
+
separate_codes = [arr.codes_from_points(line) for line in separate_lines]
|
| 436 |
+
return (separate_lines, separate_codes)
|
| 437 |
+
elif line_type_to == LineType.ChunkCombinedCode:
|
| 438 |
+
chunk_codes: list[cpy.CodeArray | None] = []
|
| 439 |
+
for points, offsets in zip(*lines):
|
| 440 |
+
if points is None:
|
| 441 |
+
chunk_codes.append(None)
|
| 442 |
+
else:
|
| 443 |
+
if TYPE_CHECKING:
|
| 444 |
+
assert offsets is not None
|
| 445 |
+
chunk_codes.append(arr.codes_from_offsets_and_points(offsets, points))
|
| 446 |
+
return (lines[0], chunk_codes)
|
| 447 |
+
elif line_type_to == LineType.ChunkCombinedOffset:
|
| 448 |
+
return lines
|
| 449 |
+
elif line_type_to == LineType.ChunkCombinedNan:
|
| 450 |
+
points_nan: list[cpy.PointArray | None] = []
|
| 451 |
+
for points, offsets in zip(*lines):
|
| 452 |
+
if points is None:
|
| 453 |
+
points_nan.append(None)
|
| 454 |
+
else:
|
| 455 |
+
if TYPE_CHECKING:
|
| 456 |
+
assert offsets is not None
|
| 457 |
+
points_nan.append(arr.insert_nan_at_offsets(points, offsets))
|
| 458 |
+
return (points_nan,)
|
| 459 |
+
else:
|
| 460 |
+
raise ValueError(f"Invalid LineType {line_type_to}")
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def _convert_lines_from_ChunkCombinedNan(
|
| 464 |
+
lines: cpy.LineReturn_ChunkCombinedNan,
|
| 465 |
+
line_type_to: LineType,
|
| 466 |
+
) -> cpy.LineReturn:
|
| 467 |
+
if line_type_to in (LineType.Separate, LineType.SeparateCode):
|
| 468 |
+
separate_lines = []
|
| 469 |
+
for points in lines[0]:
|
| 470 |
+
if points is not None:
|
| 471 |
+
separate_lines += arr.split_points_at_nan(points)
|
| 472 |
+
if line_type_to == LineType.Separate:
|
| 473 |
+
return separate_lines
|
| 474 |
+
else:
|
| 475 |
+
separate_codes = [arr.codes_from_points(points) for points in separate_lines]
|
| 476 |
+
return (separate_lines, separate_codes)
|
| 477 |
+
elif line_type_to == LineType.ChunkCombinedCode:
|
| 478 |
+
chunk_points: list[cpy.PointArray | None] = []
|
| 479 |
+
chunk_codes: list[cpy.CodeArray | None] = []
|
| 480 |
+
for points in lines[0]:
|
| 481 |
+
if points is None:
|
| 482 |
+
chunk_points.append(None)
|
| 483 |
+
chunk_codes.append(None)
|
| 484 |
+
else:
|
| 485 |
+
points, offsets = arr.remove_nan(points)
|
| 486 |
+
chunk_points.append(points)
|
| 487 |
+
chunk_codes.append(arr.codes_from_offsets_and_points(offsets, points))
|
| 488 |
+
return (chunk_points, chunk_codes)
|
| 489 |
+
elif line_type_to == LineType.ChunkCombinedOffset:
|
| 490 |
+
chunk_points = []
|
| 491 |
+
chunk_offsets: list[cpy.OffsetArray | None] = []
|
| 492 |
+
for points in lines[0]:
|
| 493 |
+
if points is None:
|
| 494 |
+
chunk_points.append(None)
|
| 495 |
+
chunk_offsets.append(None)
|
| 496 |
+
else:
|
| 497 |
+
points, offsets = arr.remove_nan(points)
|
| 498 |
+
chunk_points.append(points)
|
| 499 |
+
chunk_offsets.append(offsets)
|
| 500 |
+
return (chunk_points, chunk_offsets)
|
| 501 |
+
elif line_type_to == LineType.ChunkCombinedNan:
|
| 502 |
+
return lines
|
| 503 |
+
else:
|
| 504 |
+
raise ValueError(f"Invalid LineType {line_type_to}")
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
def convert_lines(
|
| 508 |
+
lines: cpy.LineReturn,
|
| 509 |
+
line_type_from: LineType | str,
|
| 510 |
+
line_type_to: LineType | str,
|
| 511 |
+
) -> cpy.LineReturn:
|
| 512 |
+
"""Convert contour lines from one :class:`~.LineType` to another.
|
| 513 |
+
|
| 514 |
+
Args:
|
| 515 |
+
lines (sequence of arrays): Contour lines to convert, such as those returned by
|
| 516 |
+
:meth:`.ContourGenerator.lines`.
|
| 517 |
+
line_type_from (LineType or str): :class:`~.LineType` to convert from as enum or
|
| 518 |
+
string equivalent.
|
| 519 |
+
line_type_to (LineType or str): :class:`~.LineType` to convert to as enum or string
|
| 520 |
+
equivalent.
|
| 521 |
+
|
| 522 |
+
Return:
|
| 523 |
+
Converted contour lines.
|
| 524 |
+
|
| 525 |
+
When converting non-chunked line types (``LineType.Separate`` or ``LineType.SeparateCode``) to
|
| 526 |
+
chunked ones (``LineType.ChunkCombinedCode``, ``LineType.ChunkCombinedOffset`` or
|
| 527 |
+
``LineType.ChunkCombinedNan``), all lines are placed in the first chunk. When converting in the
|
| 528 |
+
other direction, all chunk information is discarded.
|
| 529 |
+
|
| 530 |
+
.. versionadded:: 1.2.0
|
| 531 |
+
"""
|
| 532 |
+
line_type_from = as_line_type(line_type_from)
|
| 533 |
+
line_type_to = as_line_type(line_type_to)
|
| 534 |
+
|
| 535 |
+
check_lines(lines, line_type_from)
|
| 536 |
+
|
| 537 |
+
if line_type_from == LineType.Separate:
|
| 538 |
+
if TYPE_CHECKING:
|
| 539 |
+
lines = cast(cpy.LineReturn_Separate, lines)
|
| 540 |
+
return _convert_lines_from_Separate(lines, line_type_to)
|
| 541 |
+
elif line_type_from == LineType.SeparateCode:
|
| 542 |
+
if TYPE_CHECKING:
|
| 543 |
+
lines = cast(cpy.LineReturn_SeparateCode, lines)
|
| 544 |
+
return _convert_lines_from_SeparateCode(lines, line_type_to)
|
| 545 |
+
elif line_type_from == LineType.ChunkCombinedCode:
|
| 546 |
+
if TYPE_CHECKING:
|
| 547 |
+
lines = cast(cpy.LineReturn_ChunkCombinedCode, lines)
|
| 548 |
+
return _convert_lines_from_ChunkCombinedCode(lines, line_type_to)
|
| 549 |
+
elif line_type_from == LineType.ChunkCombinedOffset:
|
| 550 |
+
if TYPE_CHECKING:
|
| 551 |
+
lines = cast(cpy.LineReturn_ChunkCombinedOffset, lines)
|
| 552 |
+
return _convert_lines_from_ChunkCombinedOffset(lines, line_type_to)
|
| 553 |
+
elif line_type_from == LineType.ChunkCombinedNan:
|
| 554 |
+
if TYPE_CHECKING:
|
| 555 |
+
lines = cast(cpy.LineReturn_ChunkCombinedNan, lines)
|
| 556 |
+
return _convert_lines_from_ChunkCombinedNan(lines, line_type_to)
|
| 557 |
+
else:
|
| 558 |
+
raise ValueError(f"Invalid LineType {line_type_from}")
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
def convert_multi_filled(
|
| 562 |
+
multi_filled: list[cpy.FillReturn],
|
| 563 |
+
fill_type_from: FillType | str,
|
| 564 |
+
fill_type_to: FillType | str,
|
| 565 |
+
) -> list[cpy.FillReturn]:
|
| 566 |
+
"""Convert multiple sets of filled contours from one :class:`~.FillType` to another.
|
| 567 |
+
|
| 568 |
+
Args:
|
| 569 |
+
multi_filled (nested sequence of arrays): Filled contour polygons to convert, such as those
|
| 570 |
+
returned by :meth:`.ContourGenerator.multi_filled`.
|
| 571 |
+
fill_type_from (FillType or str): :class:`~.FillType` to convert from as enum or
|
| 572 |
+
string equivalent.
|
| 573 |
+
fill_type_to (FillType or str): :class:`~.FillType` to convert to as enum or string
|
| 574 |
+
equivalent.
|
| 575 |
+
|
| 576 |
+
Return:
|
| 577 |
+
Converted sets filled contour polygons.
|
| 578 |
+
|
| 579 |
+
When converting non-chunked fill types (``FillType.OuterCode`` or ``FillType.OuterOffset``) to
|
| 580 |
+
chunked ones, all polygons are placed in the first chunk. When converting in the other
|
| 581 |
+
direction, all chunk information is discarded. Converting a fill type that is not aware of the
|
| 582 |
+
relationship between outer boundaries and contained holes (``FillType.ChunkCombinedCode`` or
|
| 583 |
+
``FillType.ChunkCombinedOffset``) to one that is will raise a ``ValueError``.
|
| 584 |
+
|
| 585 |
+
.. versionadded:: 1.3.0
|
| 586 |
+
"""
|
| 587 |
+
fill_type_from = as_fill_type(fill_type_from)
|
| 588 |
+
fill_type_to = as_fill_type(fill_type_to)
|
| 589 |
+
|
| 590 |
+
return [convert_filled(filled, fill_type_from, fill_type_to) for filled in multi_filled]
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
def convert_multi_lines(
|
| 594 |
+
multi_lines: list[cpy.LineReturn],
|
| 595 |
+
line_type_from: LineType | str,
|
| 596 |
+
line_type_to: LineType | str,
|
| 597 |
+
) -> list[cpy.LineReturn]:
|
| 598 |
+
"""Convert multiple sets of contour lines from one :class:`~.LineType` to another.
|
| 599 |
+
|
| 600 |
+
Args:
|
| 601 |
+
multi_lines (nested sequence of arrays): Contour lines to convert, such as those returned by
|
| 602 |
+
:meth:`.ContourGenerator.multi_lines`.
|
| 603 |
+
line_type_from (LineType or str): :class:`~.LineType` to convert from as enum or
|
| 604 |
+
string equivalent.
|
| 605 |
+
line_type_to (LineType or str): :class:`~.LineType` to convert to as enum or string
|
| 606 |
+
equivalent.
|
| 607 |
+
|
| 608 |
+
Return:
|
| 609 |
+
Converted set of contour lines.
|
| 610 |
+
|
| 611 |
+
When converting non-chunked line types (``LineType.Separate`` or ``LineType.SeparateCode``) to
|
| 612 |
+
chunked ones (``LineType.ChunkCombinedCode``, ``LineType.ChunkCombinedOffset`` or
|
| 613 |
+
``LineType.ChunkCombinedNan``), all lines are placed in the first chunk. When converting in the
|
| 614 |
+
other direction, all chunk information is discarded.
|
| 615 |
+
|
| 616 |
+
.. versionadded:: 1.3.0
|
| 617 |
+
"""
|
| 618 |
+
line_type_from = as_line_type(line_type_from)
|
| 619 |
+
line_type_to = as_line_type(line_type_to)
|
| 620 |
+
|
| 621 |
+
return [convert_lines(lines, line_type_from, line_type_to) for lines in multi_lines]
|
venv/lib/python3.10/site-packages/contourpy/dechunk.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING, cast
|
| 4 |
+
|
| 5 |
+
from contourpy._contourpy import FillType, LineType
|
| 6 |
+
from contourpy.array import (
|
| 7 |
+
concat_codes_or_none,
|
| 8 |
+
concat_offsets_or_none,
|
| 9 |
+
concat_points_or_none,
|
| 10 |
+
concat_points_or_none_with_nan,
|
| 11 |
+
)
|
| 12 |
+
from contourpy.enum_util import as_fill_type, as_line_type
|
| 13 |
+
from contourpy.typecheck import check_filled, check_lines
|
| 14 |
+
|
| 15 |
+
if TYPE_CHECKING:
|
| 16 |
+
import contourpy._contourpy as cpy
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def dechunk_filled(filled: cpy.FillReturn, fill_type: FillType | str) -> cpy.FillReturn:
|
| 20 |
+
"""Return the specified filled contours with chunked data moved into the first chunk.
|
| 21 |
+
|
| 22 |
+
Filled contours that are not chunked (``FillType.OuterCode`` and ``FillType.OuterOffset``) and
|
| 23 |
+
those that are but only contain a single chunk are returned unmodified. Individual polygons are
|
| 24 |
+
unchanged, they are not geometrically combined.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
filled (sequence of arrays): Filled contour data, such as returned by
|
| 28 |
+
:meth:`.ContourGenerator.filled`.
|
| 29 |
+
fill_type (FillType or str): Type of :meth:`~.ContourGenerator.filled` as enum or string
|
| 30 |
+
equivalent.
|
| 31 |
+
|
| 32 |
+
Return:
|
| 33 |
+
Filled contours in a single chunk.
|
| 34 |
+
|
| 35 |
+
.. versionadded:: 1.2.0
|
| 36 |
+
"""
|
| 37 |
+
fill_type = as_fill_type(fill_type)
|
| 38 |
+
|
| 39 |
+
if fill_type in (FillType.OuterCode, FillType.OuterOffset):
|
| 40 |
+
# No-op if fill_type is not chunked.
|
| 41 |
+
return filled
|
| 42 |
+
|
| 43 |
+
check_filled(filled, fill_type)
|
| 44 |
+
if len(filled[0]) < 2:
|
| 45 |
+
# No-op if just one chunk.
|
| 46 |
+
return filled
|
| 47 |
+
|
| 48 |
+
if TYPE_CHECKING:
|
| 49 |
+
filled = cast(cpy.FillReturn_Chunk, filled)
|
| 50 |
+
points = concat_points_or_none(filled[0])
|
| 51 |
+
|
| 52 |
+
if fill_type == FillType.ChunkCombinedCode:
|
| 53 |
+
if TYPE_CHECKING:
|
| 54 |
+
filled = cast(cpy.FillReturn_ChunkCombinedCode, filled)
|
| 55 |
+
if points is None:
|
| 56 |
+
ret1: cpy.FillReturn_ChunkCombinedCode = ([None], [None])
|
| 57 |
+
else:
|
| 58 |
+
ret1 = ([points], [concat_codes_or_none(filled[1])])
|
| 59 |
+
return ret1
|
| 60 |
+
elif fill_type == FillType.ChunkCombinedOffset:
|
| 61 |
+
if TYPE_CHECKING:
|
| 62 |
+
filled = cast(cpy.FillReturn_ChunkCombinedOffset, filled)
|
| 63 |
+
if points is None:
|
| 64 |
+
ret2: cpy.FillReturn_ChunkCombinedOffset = ([None], [None])
|
| 65 |
+
else:
|
| 66 |
+
ret2 = ([points], [concat_offsets_or_none(filled[1])])
|
| 67 |
+
return ret2
|
| 68 |
+
elif fill_type == FillType.ChunkCombinedCodeOffset:
|
| 69 |
+
if TYPE_CHECKING:
|
| 70 |
+
filled = cast(cpy.FillReturn_ChunkCombinedCodeOffset, filled)
|
| 71 |
+
if points is None:
|
| 72 |
+
ret3: cpy.FillReturn_ChunkCombinedCodeOffset = ([None], [None], [None])
|
| 73 |
+
else:
|
| 74 |
+
outer_offsets = concat_offsets_or_none(filled[2])
|
| 75 |
+
ret3 = ([points], [concat_codes_or_none(filled[1])], [outer_offsets])
|
| 76 |
+
return ret3
|
| 77 |
+
elif fill_type == FillType.ChunkCombinedOffsetOffset:
|
| 78 |
+
if TYPE_CHECKING:
|
| 79 |
+
filled = cast(cpy.FillReturn_ChunkCombinedOffsetOffset, filled)
|
| 80 |
+
if points is None:
|
| 81 |
+
ret4: cpy.FillReturn_ChunkCombinedOffsetOffset = ([None], [None], [None])
|
| 82 |
+
else:
|
| 83 |
+
outer_offsets = concat_offsets_or_none(filled[2])
|
| 84 |
+
ret4 = ([points], [concat_offsets_or_none(filled[1])], [outer_offsets])
|
| 85 |
+
return ret4
|
| 86 |
+
else:
|
| 87 |
+
raise ValueError(f"Invalid FillType {fill_type}")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def dechunk_lines(lines: cpy.LineReturn, line_type: LineType | str) -> cpy.LineReturn:
|
| 91 |
+
"""Return the specified contour lines with chunked data moved into the first chunk.
|
| 92 |
+
|
| 93 |
+
Contour lines that are not chunked (``LineType.Separate`` and ``LineType.SeparateCode``) and
|
| 94 |
+
those that are but only contain a single chunk are returned unmodified. Individual lines are
|
| 95 |
+
unchanged, they are not geometrically combined.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
lines (sequence of arrays): Contour line data, such as returned by
|
| 99 |
+
:meth:`.ContourGenerator.lines`.
|
| 100 |
+
line_type (LineType or str): Type of :meth:`~.ContourGenerator.lines` as enum or string
|
| 101 |
+
equivalent.
|
| 102 |
+
|
| 103 |
+
Return:
|
| 104 |
+
Contour lines in a single chunk.
|
| 105 |
+
|
| 106 |
+
.. versionadded:: 1.2.0
|
| 107 |
+
"""
|
| 108 |
+
line_type = as_line_type(line_type)
|
| 109 |
+
|
| 110 |
+
if line_type in (LineType.Separate, LineType.SeparateCode):
|
| 111 |
+
# No-op if line_type is not chunked.
|
| 112 |
+
return lines
|
| 113 |
+
|
| 114 |
+
check_lines(lines, line_type)
|
| 115 |
+
if len(lines[0]) < 2:
|
| 116 |
+
# No-op if just one chunk.
|
| 117 |
+
return lines
|
| 118 |
+
|
| 119 |
+
if TYPE_CHECKING:
|
| 120 |
+
lines = cast(cpy.LineReturn_Chunk, lines)
|
| 121 |
+
|
| 122 |
+
if line_type == LineType.ChunkCombinedCode:
|
| 123 |
+
if TYPE_CHECKING:
|
| 124 |
+
lines = cast(cpy.LineReturn_ChunkCombinedCode, lines)
|
| 125 |
+
points = concat_points_or_none(lines[0])
|
| 126 |
+
if points is None:
|
| 127 |
+
ret1: cpy.LineReturn_ChunkCombinedCode = ([None], [None])
|
| 128 |
+
else:
|
| 129 |
+
ret1 = ([points], [concat_codes_or_none(lines[1])])
|
| 130 |
+
return ret1
|
| 131 |
+
elif line_type == LineType.ChunkCombinedOffset:
|
| 132 |
+
if TYPE_CHECKING:
|
| 133 |
+
lines = cast(cpy.LineReturn_ChunkCombinedOffset, lines)
|
| 134 |
+
points = concat_points_or_none(lines[0])
|
| 135 |
+
if points is None:
|
| 136 |
+
ret2: cpy.LineReturn_ChunkCombinedOffset = ([None], [None])
|
| 137 |
+
else:
|
| 138 |
+
ret2 = ([points], [concat_offsets_or_none(lines[1])])
|
| 139 |
+
return ret2
|
| 140 |
+
elif line_type == LineType.ChunkCombinedNan:
|
| 141 |
+
if TYPE_CHECKING:
|
| 142 |
+
lines = cast(cpy.LineReturn_ChunkCombinedNan, lines)
|
| 143 |
+
points = concat_points_or_none_with_nan(lines[0])
|
| 144 |
+
ret3: cpy.LineReturn_ChunkCombinedNan = ([points],)
|
| 145 |
+
return ret3
|
| 146 |
+
else:
|
| 147 |
+
raise ValueError(f"Invalid LineType {line_type}")
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def dechunk_multi_filled(
|
| 151 |
+
multi_filled: list[cpy.FillReturn],
|
| 152 |
+
fill_type: FillType | str,
|
| 153 |
+
) -> list[cpy.FillReturn]:
|
| 154 |
+
"""Return multiple sets of filled contours with chunked data moved into the first chunks.
|
| 155 |
+
|
| 156 |
+
Filled contours that are not chunked (``FillType.OuterCode`` and ``FillType.OuterOffset``) and
|
| 157 |
+
those that are but only contain a single chunk are returned unmodified. Individual polygons are
|
| 158 |
+
unchanged, they are not geometrically combined.
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
multi_filled (nested sequence of arrays): Filled contour data, such as returned by
|
| 162 |
+
:meth:`.ContourGenerator.multi_filled`.
|
| 163 |
+
fill_type (FillType or str): Type of :meth:`~.ContourGenerator.filled` as enum or string
|
| 164 |
+
equivalent.
|
| 165 |
+
|
| 166 |
+
Return:
|
| 167 |
+
Multiple sets of filled contours in a single chunk.
|
| 168 |
+
|
| 169 |
+
.. versionadded:: 1.3.0
|
| 170 |
+
"""
|
| 171 |
+
fill_type = as_fill_type(fill_type)
|
| 172 |
+
|
| 173 |
+
if fill_type in (FillType.OuterCode, FillType.OuterOffset):
|
| 174 |
+
# No-op if fill_type is not chunked.
|
| 175 |
+
return multi_filled
|
| 176 |
+
|
| 177 |
+
return [dechunk_filled(filled, fill_type) for filled in multi_filled]
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def dechunk_multi_lines(
|
| 181 |
+
multi_lines: list[cpy.LineReturn],
|
| 182 |
+
line_type: LineType | str,
|
| 183 |
+
) -> list[cpy.LineReturn]:
|
| 184 |
+
"""Return multiple sets of contour lines with all chunked data moved into the first chunks.
|
| 185 |
+
|
| 186 |
+
Contour lines that are not chunked (``LineType.Separate`` and ``LineType.SeparateCode``) and
|
| 187 |
+
those that are but only contain a single chunk are returned unmodified. Individual lines are
|
| 188 |
+
unchanged, they are not geometrically combined.
|
| 189 |
+
|
| 190 |
+
Args:
|
| 191 |
+
multi_lines (nested sequence of arrays): Contour line data, such as returned by
|
| 192 |
+
:meth:`.ContourGenerator.multi_lines`.
|
| 193 |
+
line_type (LineType or str): Type of :meth:`~.ContourGenerator.lines` as enum or string
|
| 194 |
+
equivalent.
|
| 195 |
+
|
| 196 |
+
Return:
|
| 197 |
+
Multiple sets of contour lines in a single chunk.
|
| 198 |
+
|
| 199 |
+
.. versionadded:: 1.3.0
|
| 200 |
+
"""
|
| 201 |
+
line_type = as_line_type(line_type)
|
| 202 |
+
|
| 203 |
+
if line_type in (LineType.Separate, LineType.SeparateCode):
|
| 204 |
+
# No-op if line_type is not chunked.
|
| 205 |
+
return multi_lines
|
| 206 |
+
|
| 207 |
+
return [dechunk_lines(lines, line_type) for lines in multi_lines]
|
venv/lib/python3.10/site-packages/contourpy/enum_util.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from contourpy._contourpy import FillType, LineType, ZInterp
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def as_fill_type(fill_type: FillType | str) -> FillType:
|
| 7 |
+
"""Coerce a FillType or string value to a FillType.
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
fill_type (FillType or str): Value to convert.
|
| 11 |
+
|
| 12 |
+
Return:
|
| 13 |
+
FillType: Converted value.
|
| 14 |
+
"""
|
| 15 |
+
if isinstance(fill_type, str):
|
| 16 |
+
try:
|
| 17 |
+
return FillType.__members__[fill_type]
|
| 18 |
+
except KeyError as e:
|
| 19 |
+
raise ValueError(f"'{fill_type}' is not a valid FillType") from e
|
| 20 |
+
else:
|
| 21 |
+
return fill_type
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def as_line_type(line_type: LineType | str) -> LineType:
|
| 25 |
+
"""Coerce a LineType or string value to a LineType.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
line_type (LineType or str): Value to convert.
|
| 29 |
+
|
| 30 |
+
Return:
|
| 31 |
+
LineType: Converted value.
|
| 32 |
+
"""
|
| 33 |
+
if isinstance(line_type, str):
|
| 34 |
+
try:
|
| 35 |
+
return LineType.__members__[line_type]
|
| 36 |
+
except KeyError as e:
|
| 37 |
+
raise ValueError(f"'{line_type}' is not a valid LineType") from e
|
| 38 |
+
else:
|
| 39 |
+
return line_type
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def as_z_interp(z_interp: ZInterp | str) -> ZInterp:
|
| 43 |
+
"""Coerce a ZInterp or string value to a ZInterp.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
z_interp (ZInterp or str): Value to convert.
|
| 47 |
+
|
| 48 |
+
Return:
|
| 49 |
+
ZInterp: Converted value.
|
| 50 |
+
"""
|
| 51 |
+
if isinstance(z_interp, str):
|
| 52 |
+
try:
|
| 53 |
+
return ZInterp.__members__[z_interp]
|
| 54 |
+
except KeyError as e:
|
| 55 |
+
raise ValueError(f"'{z_interp}' is not a valid ZInterp") from e
|
| 56 |
+
else:
|
| 57 |
+
return z_interp
|
venv/lib/python3.10/site-packages/contourpy/py.typed
ADDED
|
File without changes
|
venv/lib/python3.10/site-packages/contourpy/typecheck.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING, Any, cast
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
from contourpy import FillType, LineType
|
| 8 |
+
from contourpy.enum_util import as_fill_type, as_line_type
|
| 9 |
+
from contourpy.types import MOVETO, code_dtype, offset_dtype, point_dtype
|
| 10 |
+
|
| 11 |
+
if TYPE_CHECKING:
|
| 12 |
+
import contourpy._contourpy as cpy
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# Minimalist array-checking functions that check dtype, ndims and shape only.
|
| 16 |
+
# They do not walk the arrays to check the contents for performance reasons.
|
| 17 |
+
def check_code_array(codes: Any) -> None:
|
| 18 |
+
if not isinstance(codes, np.ndarray):
|
| 19 |
+
raise TypeError(f"Expected numpy array not {type(codes)}")
|
| 20 |
+
if codes.dtype != code_dtype:
|
| 21 |
+
raise ValueError(f"Expected numpy array of dtype {code_dtype} not {codes.dtype}")
|
| 22 |
+
if not (codes.ndim == 1 and len(codes) > 1):
|
| 23 |
+
raise ValueError(f"Expected numpy array of shape (?,) not {codes.shape}")
|
| 24 |
+
if codes[0] != MOVETO:
|
| 25 |
+
raise ValueError(f"First element of code array must be {MOVETO}, not {codes[0]}")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def check_offset_array(offsets: Any) -> None:
|
| 29 |
+
if not isinstance(offsets, np.ndarray):
|
| 30 |
+
raise TypeError(f"Expected numpy array not {type(offsets)}")
|
| 31 |
+
if offsets.dtype != offset_dtype:
|
| 32 |
+
raise ValueError(f"Expected numpy array of dtype {offset_dtype} not {offsets.dtype}")
|
| 33 |
+
if not (offsets.ndim == 1 and len(offsets) > 1):
|
| 34 |
+
raise ValueError(f"Expected numpy array of shape (?,) not {offsets.shape}")
|
| 35 |
+
if offsets[0] != 0:
|
| 36 |
+
raise ValueError(f"First element of offset array must be 0, not {offsets[0]}")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def check_point_array(points: Any) -> None:
|
| 40 |
+
if not isinstance(points, np.ndarray):
|
| 41 |
+
raise TypeError(f"Expected numpy array not {type(points)}")
|
| 42 |
+
if points.dtype != point_dtype:
|
| 43 |
+
raise ValueError(f"Expected numpy array of dtype {point_dtype} not {points.dtype}")
|
| 44 |
+
if not (points.ndim == 2 and points.shape[1] ==2 and points.shape[0] > 1):
|
| 45 |
+
raise ValueError(f"Expected numpy array of shape (?, 2) not {points.shape}")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _check_tuple_of_lists_with_same_length(
|
| 49 |
+
maybe_tuple: Any,
|
| 50 |
+
tuple_length: int,
|
| 51 |
+
allow_empty_lists: bool = True,
|
| 52 |
+
) -> None:
|
| 53 |
+
if not isinstance(maybe_tuple, tuple):
|
| 54 |
+
raise TypeError(f"Expected tuple not {type(maybe_tuple)}")
|
| 55 |
+
if len(maybe_tuple) != tuple_length:
|
| 56 |
+
raise ValueError(f"Expected tuple of length {tuple_length} not {len(maybe_tuple)}")
|
| 57 |
+
for maybe_list in maybe_tuple:
|
| 58 |
+
if not isinstance(maybe_list, list):
|
| 59 |
+
msg = f"Expected tuple to contain {tuple_length} lists but found a {type(maybe_list)}"
|
| 60 |
+
raise TypeError(msg)
|
| 61 |
+
lengths = [len(item) for item in maybe_tuple]
|
| 62 |
+
if len(set(lengths)) != 1:
|
| 63 |
+
msg = f"Expected {tuple_length} lists with same length but lengths are {lengths}"
|
| 64 |
+
raise ValueError(msg)
|
| 65 |
+
if not allow_empty_lists and lengths[0] == 0:
|
| 66 |
+
raise ValueError(f"Expected {tuple_length} non-empty lists")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def check_filled(filled: cpy.FillReturn, fill_type: FillType | str) -> None:
|
| 70 |
+
fill_type = as_fill_type(fill_type)
|
| 71 |
+
|
| 72 |
+
if fill_type == FillType.OuterCode:
|
| 73 |
+
if TYPE_CHECKING:
|
| 74 |
+
filled = cast(cpy.FillReturn_OuterCode, filled)
|
| 75 |
+
_check_tuple_of_lists_with_same_length(filled, 2)
|
| 76 |
+
for i, (points, codes) in enumerate(zip(*filled)):
|
| 77 |
+
check_point_array(points)
|
| 78 |
+
check_code_array(codes)
|
| 79 |
+
if len(points) != len(codes):
|
| 80 |
+
raise ValueError(f"Points and codes have different lengths in polygon {i}")
|
| 81 |
+
elif fill_type == FillType.OuterOffset:
|
| 82 |
+
if TYPE_CHECKING:
|
| 83 |
+
filled = cast(cpy.FillReturn_OuterOffset, filled)
|
| 84 |
+
_check_tuple_of_lists_with_same_length(filled, 2)
|
| 85 |
+
for i, (points, offsets) in enumerate(zip(*filled)):
|
| 86 |
+
check_point_array(points)
|
| 87 |
+
check_offset_array(offsets)
|
| 88 |
+
if offsets[-1] != len(points):
|
| 89 |
+
raise ValueError(f"Inconsistent points and offsets in polygon {i}")
|
| 90 |
+
elif fill_type == FillType.ChunkCombinedCode:
|
| 91 |
+
if TYPE_CHECKING:
|
| 92 |
+
filled = cast(cpy.FillReturn_ChunkCombinedCode, filled)
|
| 93 |
+
_check_tuple_of_lists_with_same_length(filled, 2, allow_empty_lists=False)
|
| 94 |
+
for chunk, (points_or_none, codes_or_none) in enumerate(zip(*filled)):
|
| 95 |
+
if points_or_none is not None and codes_or_none is not None:
|
| 96 |
+
check_point_array(points_or_none)
|
| 97 |
+
check_code_array(codes_or_none)
|
| 98 |
+
if len(points_or_none) != len(codes_or_none):
|
| 99 |
+
raise ValueError(f"Points and codes have different lengths in chunk {chunk}")
|
| 100 |
+
elif not (points_or_none is None and codes_or_none is None):
|
| 101 |
+
raise ValueError(f"Inconsistent Nones in chunk {chunk}")
|
| 102 |
+
elif fill_type == FillType.ChunkCombinedOffset:
|
| 103 |
+
if TYPE_CHECKING:
|
| 104 |
+
filled = cast(cpy.FillReturn_ChunkCombinedOffset, filled)
|
| 105 |
+
_check_tuple_of_lists_with_same_length(filled, 2, allow_empty_lists=False)
|
| 106 |
+
for chunk, (points_or_none, offsets_or_none) in enumerate(zip(*filled)):
|
| 107 |
+
if points_or_none is not None and offsets_or_none is not None:
|
| 108 |
+
check_point_array(points_or_none)
|
| 109 |
+
check_offset_array(offsets_or_none)
|
| 110 |
+
if offsets_or_none[-1] != len(points_or_none):
|
| 111 |
+
raise ValueError(f"Inconsistent points and offsets in chunk {chunk}")
|
| 112 |
+
elif not (points_or_none is None and offsets_or_none is None):
|
| 113 |
+
raise ValueError(f"Inconsistent Nones in chunk {chunk}")
|
| 114 |
+
elif fill_type == FillType.ChunkCombinedCodeOffset:
|
| 115 |
+
if TYPE_CHECKING:
|
| 116 |
+
filled = cast(cpy.FillReturn_ChunkCombinedCodeOffset, filled)
|
| 117 |
+
_check_tuple_of_lists_with_same_length(filled, 3, allow_empty_lists=False)
|
| 118 |
+
for i, (points_or_none, codes_or_none, outer_offsets_or_none) in enumerate(zip(*filled)):
|
| 119 |
+
if (points_or_none is not None and codes_or_none is not None and
|
| 120 |
+
outer_offsets_or_none is not None):
|
| 121 |
+
check_point_array(points_or_none)
|
| 122 |
+
check_code_array(codes_or_none)
|
| 123 |
+
check_offset_array(outer_offsets_or_none)
|
| 124 |
+
if len(codes_or_none) != len(points_or_none):
|
| 125 |
+
raise ValueError(f"Points and codes have different lengths in chunk {i}")
|
| 126 |
+
if outer_offsets_or_none[-1] != len(codes_or_none):
|
| 127 |
+
raise ValueError(f"Inconsistent codes and outer_offsets in chunk {i}")
|
| 128 |
+
elif not (points_or_none is None and codes_or_none is None and
|
| 129 |
+
outer_offsets_or_none is None):
|
| 130 |
+
raise ValueError(f"Inconsistent Nones in chunk {i}")
|
| 131 |
+
elif fill_type == FillType.ChunkCombinedOffsetOffset:
|
| 132 |
+
if TYPE_CHECKING:
|
| 133 |
+
filled = cast(cpy.FillReturn_ChunkCombinedOffsetOffset, filled)
|
| 134 |
+
_check_tuple_of_lists_with_same_length(filled, 3, allow_empty_lists=False)
|
| 135 |
+
for i, (points_or_none, offsets_or_none, outer_offsets_or_none) in enumerate(zip(*filled)):
|
| 136 |
+
if (points_or_none is not None and offsets_or_none is not None and
|
| 137 |
+
outer_offsets_or_none is not None):
|
| 138 |
+
check_point_array(points_or_none)
|
| 139 |
+
check_offset_array(offsets_or_none)
|
| 140 |
+
check_offset_array(outer_offsets_or_none)
|
| 141 |
+
if offsets_or_none[-1] != len(points_or_none):
|
| 142 |
+
raise ValueError(f"Inconsistent points and offsets in chunk {i}")
|
| 143 |
+
if outer_offsets_or_none[-1] != len(offsets_or_none) - 1:
|
| 144 |
+
raise ValueError(f"Inconsistent offsets and outer_offsets in chunk {i}")
|
| 145 |
+
elif not (points_or_none is None and offsets_or_none is None and
|
| 146 |
+
outer_offsets_or_none is None):
|
| 147 |
+
raise ValueError(f"Inconsistent Nones in chunk {i}")
|
| 148 |
+
else:
|
| 149 |
+
raise ValueError(f"Invalid FillType {fill_type}")
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def check_lines(lines: cpy.LineReturn, line_type: LineType | str) -> None:
|
| 153 |
+
line_type = as_line_type(line_type)
|
| 154 |
+
|
| 155 |
+
if line_type == LineType.Separate:
|
| 156 |
+
if TYPE_CHECKING:
|
| 157 |
+
lines = cast(cpy.LineReturn_Separate, lines)
|
| 158 |
+
if not isinstance(lines, list):
|
| 159 |
+
raise TypeError(f"Expected list not {type(lines)}")
|
| 160 |
+
for points in lines:
|
| 161 |
+
check_point_array(points)
|
| 162 |
+
elif line_type == LineType.SeparateCode:
|
| 163 |
+
if TYPE_CHECKING:
|
| 164 |
+
lines = cast(cpy.LineReturn_SeparateCode, lines)
|
| 165 |
+
_check_tuple_of_lists_with_same_length(lines, 2)
|
| 166 |
+
for i, (points, codes) in enumerate(zip(*lines)):
|
| 167 |
+
check_point_array(points)
|
| 168 |
+
check_code_array(codes)
|
| 169 |
+
if len(points) != len(codes):
|
| 170 |
+
raise ValueError(f"Points and codes have different lengths in line {i}")
|
| 171 |
+
elif line_type == LineType.ChunkCombinedCode:
|
| 172 |
+
if TYPE_CHECKING:
|
| 173 |
+
lines = cast(cpy.LineReturn_ChunkCombinedCode, lines)
|
| 174 |
+
_check_tuple_of_lists_with_same_length(lines, 2, allow_empty_lists=False)
|
| 175 |
+
for chunk, (points_or_none, codes_or_none) in enumerate(zip(*lines)):
|
| 176 |
+
if points_or_none is not None and codes_or_none is not None:
|
| 177 |
+
check_point_array(points_or_none)
|
| 178 |
+
check_code_array(codes_or_none)
|
| 179 |
+
if len(points_or_none) != len(codes_or_none):
|
| 180 |
+
raise ValueError(f"Points and codes have different lengths in chunk {chunk}")
|
| 181 |
+
elif not (points_or_none is None and codes_or_none is None):
|
| 182 |
+
raise ValueError(f"Inconsistent Nones in chunk {chunk}")
|
| 183 |
+
elif line_type == LineType.ChunkCombinedOffset:
|
| 184 |
+
if TYPE_CHECKING:
|
| 185 |
+
lines = cast(cpy.LineReturn_ChunkCombinedOffset, lines)
|
| 186 |
+
_check_tuple_of_lists_with_same_length(lines, 2, allow_empty_lists=False)
|
| 187 |
+
for chunk, (points_or_none, offsets_or_none) in enumerate(zip(*lines)):
|
| 188 |
+
if points_or_none is not None and offsets_or_none is not None:
|
| 189 |
+
check_point_array(points_or_none)
|
| 190 |
+
check_offset_array(offsets_or_none)
|
| 191 |
+
if offsets_or_none[-1] != len(points_or_none):
|
| 192 |
+
raise ValueError(f"Inconsistent points and offsets in chunk {chunk}")
|
| 193 |
+
elif not (points_or_none is None and offsets_or_none is None):
|
| 194 |
+
raise ValueError(f"Inconsistent Nones in chunk {chunk}")
|
| 195 |
+
elif line_type == LineType.ChunkCombinedNan:
|
| 196 |
+
if TYPE_CHECKING:
|
| 197 |
+
lines = cast(cpy.LineReturn_ChunkCombinedNan, lines)
|
| 198 |
+
_check_tuple_of_lists_with_same_length(lines, 1, allow_empty_lists=False)
|
| 199 |
+
for _chunk, points_or_none in enumerate(lines[0]):
|
| 200 |
+
if points_or_none is not None:
|
| 201 |
+
check_point_array(points_or_none)
|
| 202 |
+
else:
|
| 203 |
+
raise ValueError(f"Invalid LineType {line_type}")
|
venv/lib/python3.10/site-packages/contourpy/types.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
# dtypes of arrays returned by ContourPy.
|
| 6 |
+
point_dtype = np.float64
|
| 7 |
+
code_dtype = np.uint8
|
| 8 |
+
offset_dtype = np.uint32
|
| 9 |
+
|
| 10 |
+
# Kind codes used in Matplotlib Paths.
|
| 11 |
+
MOVETO = 1
|
| 12 |
+
LINETO = 2
|
| 13 |
+
CLOSEPOLY = 79
|
venv/lib/python3.10/site-packages/contourpy/util/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from contourpy.util._build_config import build_config
|
| 4 |
+
|
| 5 |
+
__all__ = ["build_config"]
|
venv/lib/python3.10/site-packages/contourpy/util/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (312 Bytes). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/util/__pycache__/_build_config.cpython-310.pyc
ADDED
|
Binary file (1.79 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/util/__pycache__/bokeh_renderer.cpython-310.pyc
ADDED
|
Binary file (13.8 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/util/__pycache__/bokeh_util.cpython-310.pyc
ADDED
|
Binary file (2.21 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/util/__pycache__/data.cpython-310.pyc
ADDED
|
Binary file (2.72 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/util/__pycache__/mpl_renderer.cpython-310.pyc
ADDED
|
Binary file (18.6 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/util/__pycache__/mpl_util.cpython-310.pyc
ADDED
|
Binary file (3.12 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/util/__pycache__/renderer.cpython-310.pyc
ADDED
|
Binary file (6.01 kB). View file
|
|
|
venv/lib/python3.10/site-packages/contourpy/util/_build_config.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# _build_config.py.in is converted into _build_config.py during the meson build process.
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def build_config() -> dict[str, str]:
|
| 7 |
+
"""
|
| 8 |
+
Return a dictionary containing build configuration settings.
|
| 9 |
+
|
| 10 |
+
All dictionary keys and values are strings, for example ``False`` is
|
| 11 |
+
returned as ``"False"``.
|
| 12 |
+
|
| 13 |
+
.. versionadded:: 1.1.0
|
| 14 |
+
"""
|
| 15 |
+
return dict(
|
| 16 |
+
# Python settings
|
| 17 |
+
python_version="3.10",
|
| 18 |
+
python_install_dir=r"/usr/local/lib/python3.10/site-packages/",
|
| 19 |
+
python_path=r"/tmp/build-env-dfa9bu3d/bin/python",
|
| 20 |
+
|
| 21 |
+
# Package versions
|
| 22 |
+
contourpy_version="1.3.2",
|
| 23 |
+
meson_version="1.7.2",
|
| 24 |
+
mesonpy_version="0.17.1",
|
| 25 |
+
pybind11_version="2.13.6",
|
| 26 |
+
|
| 27 |
+
# Misc meson settings
|
| 28 |
+
meson_backend="ninja",
|
| 29 |
+
build_dir=r"/project/.mesonpy-0roiogjz/lib/contourpy/util",
|
| 30 |
+
source_dir=r"/project/lib/contourpy/util",
|
| 31 |
+
cross_build="False",
|
| 32 |
+
|
| 33 |
+
# Build options
|
| 34 |
+
build_options=r"-Dbuildtype=release -Db_ndebug=if-release -Db_vscrt=md -Dvsenv=True --native-file=/project/.mesonpy-0roiogjz/meson-python-native-file.ini",
|
| 35 |
+
buildtype="release",
|
| 36 |
+
cpp_std="c++17",
|
| 37 |
+
debug="False",
|
| 38 |
+
optimization="3",
|
| 39 |
+
vsenv="True",
|
| 40 |
+
b_ndebug="if-release",
|
| 41 |
+
b_vscrt="from_buildtype",
|
| 42 |
+
|
| 43 |
+
# C++ compiler
|
| 44 |
+
compiler_name="gcc",
|
| 45 |
+
compiler_version="10.2.1",
|
| 46 |
+
linker_id="ld.bfd",
|
| 47 |
+
compile_command="c++",
|
| 48 |
+
|
| 49 |
+
# Host machine
|
| 50 |
+
host_cpu="x86_64",
|
| 51 |
+
host_cpu_family="x86_64",
|
| 52 |
+
host_cpu_endian="little",
|
| 53 |
+
host_cpu_system="linux",
|
| 54 |
+
|
| 55 |
+
# Build machine, same as host machine if not a cross_build
|
| 56 |
+
build_cpu="x86_64",
|
| 57 |
+
build_cpu_family="x86_64",
|
| 58 |
+
build_cpu_endian="little",
|
| 59 |
+
build_cpu_system="linux",
|
| 60 |
+
)
|
venv/lib/python3.10/site-packages/contourpy/util/bokeh_renderer.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import io
|
| 4 |
+
from typing import TYPE_CHECKING, Any
|
| 5 |
+
|
| 6 |
+
from bokeh.io import export_png, export_svg, show
|
| 7 |
+
from bokeh.io.export import get_screenshot_as_png
|
| 8 |
+
from bokeh.layouts import gridplot
|
| 9 |
+
from bokeh.models.annotations.labels import Label
|
| 10 |
+
from bokeh.palettes import Category10
|
| 11 |
+
from bokeh.plotting import figure
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
from contourpy.enum_util import as_fill_type, as_line_type
|
| 15 |
+
from contourpy.util.bokeh_util import filled_to_bokeh, lines_to_bokeh
|
| 16 |
+
from contourpy.util.renderer import Renderer
|
| 17 |
+
|
| 18 |
+
if TYPE_CHECKING:
|
| 19 |
+
from bokeh.core.enums import OutputBackendType
|
| 20 |
+
from bokeh.models import GridPlot
|
| 21 |
+
from bokeh.palettes import Palette
|
| 22 |
+
from numpy.typing import ArrayLike
|
| 23 |
+
from selenium.webdriver.remote.webdriver import WebDriver
|
| 24 |
+
|
| 25 |
+
from contourpy import FillType, LineType
|
| 26 |
+
from contourpy._contourpy import FillReturn, LineReturn
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class BokehRenderer(Renderer):
|
| 30 |
+
"""Utility renderer using Bokeh to render a grid of plots over the same (x, y) range.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
nrows (int, optional): Number of rows of plots, default ``1``.
|
| 34 |
+
ncols (int, optional): Number of columns of plots, default ``1``.
|
| 35 |
+
figsize (tuple(float, float), optional): Figure size in inches (assuming 100 dpi), default
|
| 36 |
+
``(9, 9)``.
|
| 37 |
+
show_frame (bool, optional): Whether to show frame and axes ticks, default ``True``.
|
| 38 |
+
want_svg (bool, optional): Whether output is required in SVG format or not, default
|
| 39 |
+
``False``.
|
| 40 |
+
|
| 41 |
+
Warning:
|
| 42 |
+
:class:`~.BokehRenderer`, unlike :class:`~.MplRenderer`, needs to be told in advance if
|
| 43 |
+
output to SVG format will be required later, otherwise it will assume PNG output.
|
| 44 |
+
"""
|
| 45 |
+
_figures: list[figure]
|
| 46 |
+
_layout: GridPlot
|
| 47 |
+
_palette: Palette
|
| 48 |
+
_want_svg: bool
|
| 49 |
+
|
| 50 |
+
def __init__(
|
| 51 |
+
self,
|
| 52 |
+
nrows: int = 1,
|
| 53 |
+
ncols: int = 1,
|
| 54 |
+
figsize: tuple[float, float] = (9, 9),
|
| 55 |
+
show_frame: bool = True,
|
| 56 |
+
want_svg: bool = False,
|
| 57 |
+
) -> None:
|
| 58 |
+
self._want_svg = want_svg
|
| 59 |
+
self._palette = Category10[10]
|
| 60 |
+
|
| 61 |
+
total_size = 100*np.asarray(figsize, dtype=int) # Assuming 100 dpi.
|
| 62 |
+
|
| 63 |
+
nfigures = nrows*ncols
|
| 64 |
+
self._figures = []
|
| 65 |
+
backend: OutputBackendType = "svg" if self._want_svg else "canvas"
|
| 66 |
+
for _ in range(nfigures):
|
| 67 |
+
fig = figure(output_backend=backend)
|
| 68 |
+
fig.xgrid.visible = False # type: ignore[attr-defined]
|
| 69 |
+
fig.ygrid.visible = False # type: ignore[attr-defined]
|
| 70 |
+
self._figures.append(fig)
|
| 71 |
+
if not show_frame:
|
| 72 |
+
fig.outline_line_color = None
|
| 73 |
+
fig.axis.visible = False # type: ignore[attr-defined]
|
| 74 |
+
|
| 75 |
+
self._layout = gridplot(
|
| 76 |
+
self._figures, ncols=ncols, toolbar_location=None, # type: ignore[arg-type]
|
| 77 |
+
width=total_size[0] // ncols, height=total_size[1] // nrows)
|
| 78 |
+
|
| 79 |
+
def _convert_color(self, color: str) -> str:
|
| 80 |
+
if isinstance(color, str) and color[0] == "C":
|
| 81 |
+
index = int(color[1:])
|
| 82 |
+
color = self._palette[index]
|
| 83 |
+
return color
|
| 84 |
+
|
| 85 |
+
def _get_figure(self, ax: figure | int) -> figure:
|
| 86 |
+
if isinstance(ax, int):
|
| 87 |
+
ax = self._figures[ax]
|
| 88 |
+
return ax
|
| 89 |
+
|
| 90 |
+
def filled(
|
| 91 |
+
self,
|
| 92 |
+
filled: FillReturn,
|
| 93 |
+
fill_type: FillType | str,
|
| 94 |
+
ax: figure | int = 0,
|
| 95 |
+
color: str = "C0",
|
| 96 |
+
alpha: float = 0.7,
|
| 97 |
+
) -> None:
|
| 98 |
+
"""Plot filled contours on a single plot.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
filled (sequence of arrays): Filled contour data as returned by
|
| 102 |
+
:meth:`~.ContourGenerator.filled`.
|
| 103 |
+
fill_type (FillType or str): Type of :meth:`~.ContourGenerator.filled` data as returned
|
| 104 |
+
by :attr:`~.ContourGenerator.fill_type`, or a string equivalent.
|
| 105 |
+
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
|
| 106 |
+
color (str, optional): Color to plot with. May be a string color or the letter ``"C"``
|
| 107 |
+
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
|
| 108 |
+
``Category10`` palette. Default ``"C0"``.
|
| 109 |
+
alpha (float, optional): Opacity to plot with, default ``0.7``.
|
| 110 |
+
"""
|
| 111 |
+
fill_type = as_fill_type(fill_type)
|
| 112 |
+
fig = self._get_figure(ax)
|
| 113 |
+
color = self._convert_color(color)
|
| 114 |
+
xs, ys = filled_to_bokeh(filled, fill_type)
|
| 115 |
+
if len(xs) > 0:
|
| 116 |
+
fig.multi_polygons(xs=[xs], ys=[ys], color=color, fill_alpha=alpha, line_width=0) # type: ignore[arg-type]
|
| 117 |
+
|
| 118 |
+
def grid(
|
| 119 |
+
self,
|
| 120 |
+
x: ArrayLike,
|
| 121 |
+
y: ArrayLike,
|
| 122 |
+
ax: figure | int = 0,
|
| 123 |
+
color: str = "black",
|
| 124 |
+
alpha: float = 0.1,
|
| 125 |
+
point_color: str | None = None,
|
| 126 |
+
quad_as_tri_alpha: float = 0,
|
| 127 |
+
) -> None:
|
| 128 |
+
"""Plot quad grid lines on a single plot.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
|
| 132 |
+
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
|
| 133 |
+
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
|
| 134 |
+
color (str, optional): Color to plot grid lines, default ``"black"``.
|
| 135 |
+
alpha (float, optional): Opacity to plot lines with, default ``0.1``.
|
| 136 |
+
point_color (str, optional): Color to plot grid points or ``None`` if grid points
|
| 137 |
+
should not be plotted, default ``None``.
|
| 138 |
+
quad_as_tri_alpha (float, optional): Opacity to plot ``quad_as_tri`` grid, default
|
| 139 |
+
``0``.
|
| 140 |
+
|
| 141 |
+
Colors may be a string color or the letter ``"C"`` followed by an integer in the range
|
| 142 |
+
``"C0"`` to ``"C9"`` to use a color from the ``Category10`` palette.
|
| 143 |
+
|
| 144 |
+
Warning:
|
| 145 |
+
``quad_as_tri_alpha > 0`` plots all quads as though they are unmasked.
|
| 146 |
+
"""
|
| 147 |
+
fig = self._get_figure(ax)
|
| 148 |
+
x, y = self._grid_as_2d(x, y)
|
| 149 |
+
xs = list(x) + list(x.T)
|
| 150 |
+
ys = list(y) + list(y.T)
|
| 151 |
+
kwargs = {"line_color": color, "alpha": alpha}
|
| 152 |
+
fig.multi_line(xs, ys, **kwargs)
|
| 153 |
+
if quad_as_tri_alpha > 0:
|
| 154 |
+
# Assumes no quad mask.
|
| 155 |
+
xmid = (0.25*(x[:-1, :-1] + x[1:, :-1] + x[:-1, 1:] + x[1:, 1:])).ravel()
|
| 156 |
+
ymid = (0.25*(y[:-1, :-1] + y[1:, :-1] + y[:-1, 1:] + y[1:, 1:])).ravel()
|
| 157 |
+
fig.multi_line(
|
| 158 |
+
list(np.stack((x[:-1, :-1].ravel(), xmid, x[1:, 1:].ravel()), axis=1)),
|
| 159 |
+
list(np.stack((y[:-1, :-1].ravel(), ymid, y[1:, 1:].ravel()), axis=1)),
|
| 160 |
+
**kwargs)
|
| 161 |
+
fig.multi_line(
|
| 162 |
+
list(np.stack((x[:-1, 1:].ravel(), xmid, x[1:, :-1].ravel()), axis=1)),
|
| 163 |
+
list(np.stack((y[:-1, 1:].ravel(), ymid, y[1:, :-1].ravel()), axis=1)),
|
| 164 |
+
**kwargs)
|
| 165 |
+
if point_color is not None:
|
| 166 |
+
fig.scatter(
|
| 167 |
+
x=x.ravel(), y=y.ravel(), fill_color=color, line_color=None, alpha=alpha,
|
| 168 |
+
marker="circle", size=8)
|
| 169 |
+
|
| 170 |
+
def lines(
|
| 171 |
+
self,
|
| 172 |
+
lines: LineReturn,
|
| 173 |
+
line_type: LineType | str,
|
| 174 |
+
ax: figure | int = 0,
|
| 175 |
+
color: str = "C0",
|
| 176 |
+
alpha: float = 1.0,
|
| 177 |
+
linewidth: float = 1,
|
| 178 |
+
) -> None:
|
| 179 |
+
"""Plot contour lines on a single plot.
|
| 180 |
+
|
| 181 |
+
Args:
|
| 182 |
+
lines (sequence of arrays): Contour line data as returned by
|
| 183 |
+
:meth:`~.ContourGenerator.lines`.
|
| 184 |
+
line_type (LineType or str): Type of :meth:`~.ContourGenerator.lines` data as returned
|
| 185 |
+
by :attr:`~.ContourGenerator.line_type`, or a string equivalent.
|
| 186 |
+
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
|
| 187 |
+
color (str, optional): Color to plot lines. May be a string color or the letter ``"C"``
|
| 188 |
+
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
|
| 189 |
+
``Category10`` palette. Default ``"C0"``.
|
| 190 |
+
alpha (float, optional): Opacity to plot lines with, default ``1.0``.
|
| 191 |
+
linewidth (float, optional): Width of lines, default ``1``.
|
| 192 |
+
|
| 193 |
+
Note:
|
| 194 |
+
Assumes all lines are open line strips not closed line loops.
|
| 195 |
+
"""
|
| 196 |
+
line_type = as_line_type(line_type)
|
| 197 |
+
fig = self._get_figure(ax)
|
| 198 |
+
color = self._convert_color(color)
|
| 199 |
+
xs, ys = lines_to_bokeh(lines, line_type)
|
| 200 |
+
if xs is not None:
|
| 201 |
+
assert ys is not None
|
| 202 |
+
fig.line(xs, ys, line_color=color, line_alpha=alpha, line_width=linewidth)
|
| 203 |
+
|
| 204 |
+
def mask(
|
| 205 |
+
self,
|
| 206 |
+
x: ArrayLike,
|
| 207 |
+
y: ArrayLike,
|
| 208 |
+
z: ArrayLike | np.ma.MaskedArray[Any, Any],
|
| 209 |
+
ax: figure | int = 0,
|
| 210 |
+
color: str = "black",
|
| 211 |
+
) -> None:
|
| 212 |
+
"""Plot masked out grid points as circles on a single plot.
|
| 213 |
+
|
| 214 |
+
Args:
|
| 215 |
+
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
|
| 216 |
+
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
|
| 217 |
+
z (masked array of shape (ny, nx): z-values.
|
| 218 |
+
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
|
| 219 |
+
color (str, optional): Circle color, default ``"black"``.
|
| 220 |
+
"""
|
| 221 |
+
mask = np.ma.getmask(z) # type: ignore[no-untyped-call]
|
| 222 |
+
if mask is np.ma.nomask:
|
| 223 |
+
return
|
| 224 |
+
fig = self._get_figure(ax)
|
| 225 |
+
color = self._convert_color(color)
|
| 226 |
+
x, y = self._grid_as_2d(x, y)
|
| 227 |
+
fig.scatter(x[mask], y[mask], fill_color=color, marker="circle", size=10)
|
| 228 |
+
|
| 229 |
+
def save(
|
| 230 |
+
self,
|
| 231 |
+
filename: str,
|
| 232 |
+
transparent: bool = False,
|
| 233 |
+
*,
|
| 234 |
+
webdriver: WebDriver | None = None,
|
| 235 |
+
) -> None:
|
| 236 |
+
"""Save plots to SVG or PNG file.
|
| 237 |
+
|
| 238 |
+
Args:
|
| 239 |
+
filename (str): Filename to save to.
|
| 240 |
+
transparent (bool, optional): Whether background should be transparent, default
|
| 241 |
+
``False``.
|
| 242 |
+
webdriver (WebDriver, optional): Selenium WebDriver instance to use to create the image.
|
| 243 |
+
|
| 244 |
+
.. versionadded:: 1.1.1
|
| 245 |
+
|
| 246 |
+
Warning:
|
| 247 |
+
To output to SVG file, ``want_svg=True`` must have been passed to the constructor.
|
| 248 |
+
"""
|
| 249 |
+
if transparent:
|
| 250 |
+
for fig in self._figures:
|
| 251 |
+
fig.background_fill_color = None
|
| 252 |
+
fig.border_fill_color = None
|
| 253 |
+
|
| 254 |
+
if self._want_svg:
|
| 255 |
+
export_svg(self._layout, filename=filename, webdriver=webdriver)
|
| 256 |
+
else:
|
| 257 |
+
export_png(self._layout, filename=filename, webdriver=webdriver)
|
| 258 |
+
|
| 259 |
+
def save_to_buffer(self, *, webdriver: WebDriver | None = None) -> io.BytesIO:
|
| 260 |
+
"""Save plots to an ``io.BytesIO`` buffer.
|
| 261 |
+
|
| 262 |
+
Args:
|
| 263 |
+
webdriver (WebDriver, optional): Selenium WebDriver instance to use to create the image.
|
| 264 |
+
|
| 265 |
+
.. versionadded:: 1.1.1
|
| 266 |
+
|
| 267 |
+
Return:
|
| 268 |
+
BytesIO: PNG image buffer.
|
| 269 |
+
"""
|
| 270 |
+
image = get_screenshot_as_png(self._layout, driver=webdriver)
|
| 271 |
+
buffer = io.BytesIO()
|
| 272 |
+
image.save(buffer, "png")
|
| 273 |
+
return buffer
|
| 274 |
+
|
| 275 |
+
def show(self) -> None:
|
| 276 |
+
"""Show plots in web browser, in usual Bokeh manner.
|
| 277 |
+
"""
|
| 278 |
+
show(self._layout)
|
| 279 |
+
|
| 280 |
+
def title(self, title: str, ax: figure | int = 0, color: str | None = None) -> None:
|
| 281 |
+
"""Set the title of a single plot.
|
| 282 |
+
|
| 283 |
+
Args:
|
| 284 |
+
title (str): Title text.
|
| 285 |
+
ax (int or Bokeh Figure, optional): Which plot to set the title of, default ``0``.
|
| 286 |
+
color (str, optional): Color to set title. May be a string color or the letter ``"C"``
|
| 287 |
+
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
|
| 288 |
+
``Category10`` palette. Default ``None`` which is ``black``.
|
| 289 |
+
"""
|
| 290 |
+
fig = self._get_figure(ax)
|
| 291 |
+
fig.title = title
|
| 292 |
+
fig.title.align = "center" # type: ignore[attr-defined]
|
| 293 |
+
if color is not None:
|
| 294 |
+
fig.title.text_color = self._convert_color(color) # type: ignore[attr-defined]
|
| 295 |
+
|
| 296 |
+
def z_values(
|
| 297 |
+
self,
|
| 298 |
+
x: ArrayLike,
|
| 299 |
+
y: ArrayLike,
|
| 300 |
+
z: ArrayLike,
|
| 301 |
+
ax: figure | int = 0,
|
| 302 |
+
color: str = "green",
|
| 303 |
+
fmt: str = ".1f",
|
| 304 |
+
quad_as_tri: bool = False,
|
| 305 |
+
) -> None:
|
| 306 |
+
"""Show ``z`` values on a single plot.
|
| 307 |
+
|
| 308 |
+
Args:
|
| 309 |
+
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
|
| 310 |
+
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
|
| 311 |
+
z (array-like of shape (ny, nx): z-values.
|
| 312 |
+
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
|
| 313 |
+
color (str, optional): Color of added text. May be a string color or the letter ``"C"``
|
| 314 |
+
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
|
| 315 |
+
``Category10`` palette. Default ``"green"``.
|
| 316 |
+
fmt (str, optional): Format to display z-values, default ``".1f"``.
|
| 317 |
+
quad_as_tri (bool, optional): Whether to show z-values at the ``quad_as_tri`` centres
|
| 318 |
+
of quads.
|
| 319 |
+
|
| 320 |
+
Warning:
|
| 321 |
+
``quad_as_tri=True`` shows z-values for all quads, even if masked.
|
| 322 |
+
"""
|
| 323 |
+
fig = self._get_figure(ax)
|
| 324 |
+
color = self._convert_color(color)
|
| 325 |
+
x, y = self._grid_as_2d(x, y)
|
| 326 |
+
z = np.asarray(z)
|
| 327 |
+
ny, nx = z.shape
|
| 328 |
+
kwargs = {"text_color": color, "text_align": "center", "text_baseline": "middle"}
|
| 329 |
+
for j in range(ny):
|
| 330 |
+
for i in range(nx):
|
| 331 |
+
label = Label(x=x[j, i], y=y[j, i], text=f"{z[j, i]:{fmt}}", **kwargs) # type: ignore[arg-type]
|
| 332 |
+
fig.add_layout(label)
|
| 333 |
+
if quad_as_tri:
|
| 334 |
+
for j in range(ny-1):
|
| 335 |
+
for i in range(nx-1):
|
| 336 |
+
xx = np.mean(x[j:j+2, i:i+2])
|
| 337 |
+
yy = np.mean(y[j:j+2, i:i+2])
|
| 338 |
+
zz = np.mean(z[j:j+2, i:i+2])
|
| 339 |
+
fig.add_layout(Label(x=xx, y=yy, text=f"{zz:{fmt}}", **kwargs)) # type: ignore[arg-type]
|
venv/lib/python3.10/site-packages/contourpy/util/bokeh_util.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING, cast
|
| 4 |
+
|
| 5 |
+
from contourpy import FillType, LineType
|
| 6 |
+
from contourpy.array import offsets_from_codes
|
| 7 |
+
from contourpy.convert import convert_lines
|
| 8 |
+
from contourpy.dechunk import dechunk_lines
|
| 9 |
+
|
| 10 |
+
if TYPE_CHECKING:
|
| 11 |
+
from contourpy._contourpy import (
|
| 12 |
+
CoordinateArray,
|
| 13 |
+
FillReturn,
|
| 14 |
+
LineReturn,
|
| 15 |
+
LineReturn_ChunkCombinedNan,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def filled_to_bokeh(
|
| 20 |
+
filled: FillReturn,
|
| 21 |
+
fill_type: FillType,
|
| 22 |
+
) -> tuple[list[list[CoordinateArray]], list[list[CoordinateArray]]]:
|
| 23 |
+
xs: list[list[CoordinateArray]] = []
|
| 24 |
+
ys: list[list[CoordinateArray]] = []
|
| 25 |
+
if fill_type in (FillType.OuterOffset, FillType.ChunkCombinedOffset,
|
| 26 |
+
FillType.OuterCode, FillType.ChunkCombinedCode):
|
| 27 |
+
have_codes = fill_type in (FillType.OuterCode, FillType.ChunkCombinedCode)
|
| 28 |
+
|
| 29 |
+
for points, offsets in zip(*filled):
|
| 30 |
+
if points is None:
|
| 31 |
+
continue
|
| 32 |
+
if have_codes:
|
| 33 |
+
offsets = offsets_from_codes(offsets)
|
| 34 |
+
xs.append([]) # New outer with zero or more holes.
|
| 35 |
+
ys.append([])
|
| 36 |
+
for i in range(len(offsets)-1):
|
| 37 |
+
xys = points[offsets[i]:offsets[i+1]]
|
| 38 |
+
xs[-1].append(xys[:, 0])
|
| 39 |
+
ys[-1].append(xys[:, 1])
|
| 40 |
+
elif fill_type in (FillType.ChunkCombinedCodeOffset, FillType.ChunkCombinedOffsetOffset):
|
| 41 |
+
for points, codes_or_offsets, outer_offsets in zip(*filled):
|
| 42 |
+
if points is None:
|
| 43 |
+
continue
|
| 44 |
+
for j in range(len(outer_offsets)-1):
|
| 45 |
+
if fill_type == FillType.ChunkCombinedCodeOffset:
|
| 46 |
+
codes = codes_or_offsets[outer_offsets[j]:outer_offsets[j+1]]
|
| 47 |
+
offsets = offsets_from_codes(codes) + outer_offsets[j]
|
| 48 |
+
else:
|
| 49 |
+
offsets = codes_or_offsets[outer_offsets[j]:outer_offsets[j+1]+1]
|
| 50 |
+
xs.append([]) # New outer with zero or more holes.
|
| 51 |
+
ys.append([])
|
| 52 |
+
for k in range(len(offsets)-1):
|
| 53 |
+
xys = points[offsets[k]:offsets[k+1]]
|
| 54 |
+
xs[-1].append(xys[:, 0])
|
| 55 |
+
ys[-1].append(xys[:, 1])
|
| 56 |
+
else:
|
| 57 |
+
raise RuntimeError(f"Conversion of FillType {fill_type} to Bokeh is not implemented")
|
| 58 |
+
|
| 59 |
+
return xs, ys
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def lines_to_bokeh(
|
| 63 |
+
lines: LineReturn,
|
| 64 |
+
line_type: LineType,
|
| 65 |
+
) -> tuple[CoordinateArray | None, CoordinateArray | None]:
|
| 66 |
+
lines = convert_lines(lines, line_type, LineType.ChunkCombinedNan)
|
| 67 |
+
lines = dechunk_lines(lines, LineType.ChunkCombinedNan)
|
| 68 |
+
if TYPE_CHECKING:
|
| 69 |
+
lines = cast(LineReturn_ChunkCombinedNan, lines)
|
| 70 |
+
points = lines[0][0]
|
| 71 |
+
if points is None:
|
| 72 |
+
return None, None
|
| 73 |
+
else:
|
| 74 |
+
return points[:, 0], points[:, 1]
|
venv/lib/python3.10/site-packages/contourpy/util/data.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING, Any
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from contourpy._contourpy import CoordinateArray
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def simple(
|
| 12 |
+
shape: tuple[int, int], want_mask: bool = False,
|
| 13 |
+
) -> tuple[CoordinateArray, CoordinateArray, CoordinateArray | np.ma.MaskedArray[Any, Any]]:
|
| 14 |
+
"""Return simple test data consisting of the sum of two gaussians.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
shape (tuple(int, int)): 2D shape of data to return.
|
| 18 |
+
want_mask (bool, optional): Whether test data should be masked or not, default ``False``.
|
| 19 |
+
|
| 20 |
+
Return:
|
| 21 |
+
Tuple of 3 arrays: ``x``, ``y``, ``z`` test data, ``z`` will be masked if
|
| 22 |
+
``want_mask=True``.
|
| 23 |
+
"""
|
| 24 |
+
ny, nx = shape
|
| 25 |
+
x = np.arange(nx, dtype=np.float64)
|
| 26 |
+
y = np.arange(ny, dtype=np.float64)
|
| 27 |
+
x, y = np.meshgrid(x, y)
|
| 28 |
+
|
| 29 |
+
xscale = nx - 1.0
|
| 30 |
+
yscale = ny - 1.0
|
| 31 |
+
|
| 32 |
+
# z is sum of 2D gaussians.
|
| 33 |
+
amp = np.asarray([1.0, -1.0, 0.8, -0.9, 0.7])
|
| 34 |
+
mid = np.asarray([[0.4, 0.2], [0.3, 0.8], [0.9, 0.75], [0.7, 0.3], [0.05, 0.7]])
|
| 35 |
+
width = np.asarray([0.4, 0.2, 0.2, 0.2, 0.1])
|
| 36 |
+
|
| 37 |
+
z = np.zeros_like(x)
|
| 38 |
+
for i in range(len(amp)):
|
| 39 |
+
z += amp[i]*np.exp(-((x/xscale - mid[i, 0])**2 + (y/yscale - mid[i, 1])**2) / width[i]**2)
|
| 40 |
+
|
| 41 |
+
if want_mask:
|
| 42 |
+
mask = np.logical_or(
|
| 43 |
+
((x/xscale - 1.0)**2 / 0.2 + (y/yscale - 0.0)**2 / 0.1) < 1.0,
|
| 44 |
+
((x/xscale - 0.2)**2 / 0.02 + (y/yscale - 0.45)**2 / 0.08) < 1.0,
|
| 45 |
+
)
|
| 46 |
+
z = np.ma.array(z, mask=mask) # type: ignore[no-untyped-call]
|
| 47 |
+
|
| 48 |
+
return x, y, z
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def random(
|
| 52 |
+
shape: tuple[int, int], seed: int = 2187, mask_fraction: float = 0.0,
|
| 53 |
+
) -> tuple[CoordinateArray, CoordinateArray, CoordinateArray | np.ma.MaskedArray[Any, Any]]:
|
| 54 |
+
"""Return random test data in the range 0 to 1.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
shape (tuple(int, int)): 2D shape of data to return.
|
| 58 |
+
seed (int, optional): Seed for random number generator, default 2187.
|
| 59 |
+
mask_fraction (float, optional): Fraction of elements to mask, default 0.
|
| 60 |
+
|
| 61 |
+
Return:
|
| 62 |
+
Tuple of 3 arrays: ``x``, ``y``, ``z`` test data, ``z`` will be masked if
|
| 63 |
+
``mask_fraction`` is greater than zero.
|
| 64 |
+
"""
|
| 65 |
+
ny, nx = shape
|
| 66 |
+
x = np.arange(nx, dtype=np.float64)
|
| 67 |
+
y = np.arange(ny, dtype=np.float64)
|
| 68 |
+
x, y = np.meshgrid(x, y)
|
| 69 |
+
|
| 70 |
+
rng = np.random.default_rng(seed)
|
| 71 |
+
z = rng.uniform(size=shape)
|
| 72 |
+
|
| 73 |
+
if mask_fraction > 0.0:
|
| 74 |
+
mask_fraction = min(mask_fraction, 0.99)
|
| 75 |
+
mask = rng.uniform(size=shape) < mask_fraction
|
| 76 |
+
z = np.ma.array(z, mask=mask) # type: ignore[no-untyped-call]
|
| 77 |
+
|
| 78 |
+
return x, y, z
|
venv/lib/python3.10/site-packages/contourpy/util/mpl_renderer.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import io
|
| 4 |
+
from itertools import pairwise
|
| 5 |
+
from typing import TYPE_CHECKING, Any, cast
|
| 6 |
+
|
| 7 |
+
import matplotlib.collections as mcollections
|
| 8 |
+
import matplotlib.pyplot as plt
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
from contourpy import FillType, LineType
|
| 12 |
+
from contourpy.convert import convert_filled, convert_lines
|
| 13 |
+
from contourpy.enum_util import as_fill_type, as_line_type
|
| 14 |
+
from contourpy.util.mpl_util import filled_to_mpl_paths, lines_to_mpl_paths
|
| 15 |
+
from contourpy.util.renderer import Renderer
|
| 16 |
+
|
| 17 |
+
if TYPE_CHECKING:
|
| 18 |
+
from collections.abc import Sequence
|
| 19 |
+
|
| 20 |
+
from matplotlib.axes import Axes
|
| 21 |
+
from matplotlib.figure import Figure
|
| 22 |
+
from numpy.typing import ArrayLike
|
| 23 |
+
|
| 24 |
+
import contourpy._contourpy as cpy
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class MplRenderer(Renderer):
|
| 28 |
+
"""Utility renderer using Matplotlib to render a grid of plots over the same (x, y) range.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
nrows (int, optional): Number of rows of plots, default ``1``.
|
| 32 |
+
ncols (int, optional): Number of columns of plots, default ``1``.
|
| 33 |
+
figsize (tuple(float, float), optional): Figure size in inches, default ``(9, 9)``.
|
| 34 |
+
show_frame (bool, optional): Whether to show frame and axes ticks, default ``True``.
|
| 35 |
+
backend (str, optional): Matplotlib backend to use or ``None`` for default backend.
|
| 36 |
+
Default ``None``.
|
| 37 |
+
gridspec_kw (dict, optional): Gridspec keyword arguments to pass to ``plt.subplots``,
|
| 38 |
+
default None.
|
| 39 |
+
"""
|
| 40 |
+
_axes: Sequence[Axes]
|
| 41 |
+
_fig: Figure
|
| 42 |
+
_want_tight: bool
|
| 43 |
+
|
| 44 |
+
def __init__(
|
| 45 |
+
self,
|
| 46 |
+
nrows: int = 1,
|
| 47 |
+
ncols: int = 1,
|
| 48 |
+
figsize: tuple[float, float] = (9, 9),
|
| 49 |
+
show_frame: bool = True,
|
| 50 |
+
backend: str | None = None,
|
| 51 |
+
gridspec_kw: dict[str, Any] | None = None,
|
| 52 |
+
) -> None:
|
| 53 |
+
if backend is not None:
|
| 54 |
+
import matplotlib as mpl
|
| 55 |
+
mpl.use(backend)
|
| 56 |
+
|
| 57 |
+
kwargs: dict[str, Any] = {"figsize": figsize, "squeeze": False,
|
| 58 |
+
"sharex": True, "sharey": True}
|
| 59 |
+
if gridspec_kw is not None:
|
| 60 |
+
kwargs["gridspec_kw"] = gridspec_kw
|
| 61 |
+
else:
|
| 62 |
+
kwargs["subplot_kw"] = {"aspect": "equal"}
|
| 63 |
+
|
| 64 |
+
self._fig, axes = plt.subplots(nrows, ncols, **kwargs)
|
| 65 |
+
self._axes = axes.flatten()
|
| 66 |
+
if not show_frame:
|
| 67 |
+
for ax in self._axes:
|
| 68 |
+
ax.axis("off")
|
| 69 |
+
|
| 70 |
+
self._want_tight = True
|
| 71 |
+
|
| 72 |
+
def __del__(self) -> None:
|
| 73 |
+
if hasattr(self, "_fig"):
|
| 74 |
+
plt.close(self._fig)
|
| 75 |
+
|
| 76 |
+
def _autoscale(self) -> None:
|
| 77 |
+
# Using axes._need_autoscale attribute if need to autoscale before rendering after adding
|
| 78 |
+
# lines/filled. Only want to autoscale once per axes regardless of how many lines/filled
|
| 79 |
+
# added.
|
| 80 |
+
for ax in self._axes:
|
| 81 |
+
if getattr(ax, "_need_autoscale", False):
|
| 82 |
+
ax.autoscale_view(tight=True)
|
| 83 |
+
ax._need_autoscale = False # type: ignore[attr-defined]
|
| 84 |
+
if self._want_tight and len(self._axes) > 1:
|
| 85 |
+
self._fig.tight_layout()
|
| 86 |
+
|
| 87 |
+
def _get_ax(self, ax: Axes | int) -> Axes:
|
| 88 |
+
if isinstance(ax, int):
|
| 89 |
+
ax = self._axes[ax]
|
| 90 |
+
return ax
|
| 91 |
+
|
| 92 |
+
def filled(
|
| 93 |
+
self,
|
| 94 |
+
filled: cpy.FillReturn,
|
| 95 |
+
fill_type: FillType | str,
|
| 96 |
+
ax: Axes | int = 0,
|
| 97 |
+
color: str = "C0",
|
| 98 |
+
alpha: float = 0.7,
|
| 99 |
+
) -> None:
|
| 100 |
+
"""Plot filled contours on a single Axes.
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
filled (sequence of arrays): Filled contour data as returned by
|
| 104 |
+
:meth:`~.ContourGenerator.filled`.
|
| 105 |
+
fill_type (FillType or str): Type of :meth:`~.ContourGenerator.filled` data as returned
|
| 106 |
+
by :attr:`~.ContourGenerator.fill_type`, or string equivalent
|
| 107 |
+
ax (int or Maplotlib Axes, optional): Which axes to plot on, default ``0``.
|
| 108 |
+
color (str, optional): Color to plot with. May be a string color or the letter ``"C"``
|
| 109 |
+
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
|
| 110 |
+
``tab10`` colormap. Default ``"C0"``.
|
| 111 |
+
alpha (float, optional): Opacity to plot with, default ``0.7``.
|
| 112 |
+
"""
|
| 113 |
+
fill_type = as_fill_type(fill_type)
|
| 114 |
+
ax = self._get_ax(ax)
|
| 115 |
+
paths = filled_to_mpl_paths(filled, fill_type)
|
| 116 |
+
collection = mcollections.PathCollection(
|
| 117 |
+
paths, facecolors=color, edgecolors="none", lw=0, alpha=alpha)
|
| 118 |
+
ax.add_collection(collection)
|
| 119 |
+
ax._need_autoscale = True # type: ignore[attr-defined]
|
| 120 |
+
|
| 121 |
+
def grid(
|
| 122 |
+
self,
|
| 123 |
+
x: ArrayLike,
|
| 124 |
+
y: ArrayLike,
|
| 125 |
+
ax: Axes | int = 0,
|
| 126 |
+
color: str = "black",
|
| 127 |
+
alpha: float = 0.1,
|
| 128 |
+
point_color: str | None = None,
|
| 129 |
+
quad_as_tri_alpha: float = 0,
|
| 130 |
+
) -> None:
|
| 131 |
+
"""Plot quad grid lines on a single Axes.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
|
| 135 |
+
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
|
| 136 |
+
ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
|
| 137 |
+
color (str, optional): Color to plot grid lines, default ``"black"``.
|
| 138 |
+
alpha (float, optional): Opacity to plot lines with, default ``0.1``.
|
| 139 |
+
point_color (str, optional): Color to plot grid points or ``None`` if grid points
|
| 140 |
+
should not be plotted, default ``None``.
|
| 141 |
+
quad_as_tri_alpha (float, optional): Opacity to plot ``quad_as_tri`` grid, default 0.
|
| 142 |
+
|
| 143 |
+
Colors may be a string color or the letter ``"C"`` followed by an integer in the range
|
| 144 |
+
``"C0"`` to ``"C9"`` to use a color from the ``tab10`` colormap.
|
| 145 |
+
|
| 146 |
+
Warning:
|
| 147 |
+
``quad_as_tri_alpha > 0`` plots all quads as though they are unmasked.
|
| 148 |
+
"""
|
| 149 |
+
ax = self._get_ax(ax)
|
| 150 |
+
x, y = self._grid_as_2d(x, y)
|
| 151 |
+
kwargs: dict[str, Any] = {"color": color, "alpha": alpha}
|
| 152 |
+
ax.plot(x, y, x.T, y.T, **kwargs)
|
| 153 |
+
if quad_as_tri_alpha > 0:
|
| 154 |
+
# Assumes no quad mask.
|
| 155 |
+
xmid = 0.25*(x[:-1, :-1] + x[1:, :-1] + x[:-1, 1:] + x[1:, 1:])
|
| 156 |
+
ymid = 0.25*(y[:-1, :-1] + y[1:, :-1] + y[:-1, 1:] + y[1:, 1:])
|
| 157 |
+
kwargs["alpha"] = quad_as_tri_alpha
|
| 158 |
+
ax.plot(
|
| 159 |
+
np.stack((x[:-1, :-1], xmid, x[1:, 1:])).reshape((3, -1)),
|
| 160 |
+
np.stack((y[:-1, :-1], ymid, y[1:, 1:])).reshape((3, -1)),
|
| 161 |
+
np.stack((x[1:, :-1], xmid, x[:-1, 1:])).reshape((3, -1)),
|
| 162 |
+
np.stack((y[1:, :-1], ymid, y[:-1, 1:])).reshape((3, -1)),
|
| 163 |
+
**kwargs)
|
| 164 |
+
if point_color is not None:
|
| 165 |
+
ax.plot(x, y, color=point_color, alpha=alpha, marker="o", lw=0)
|
| 166 |
+
ax._need_autoscale = True # type: ignore[attr-defined]
|
| 167 |
+
|
| 168 |
+
def lines(
|
| 169 |
+
self,
|
| 170 |
+
lines: cpy.LineReturn,
|
| 171 |
+
line_type: LineType | str,
|
| 172 |
+
ax: Axes | int = 0,
|
| 173 |
+
color: str = "C0",
|
| 174 |
+
alpha: float = 1.0,
|
| 175 |
+
linewidth: float = 1,
|
| 176 |
+
) -> None:
|
| 177 |
+
"""Plot contour lines on a single Axes.
|
| 178 |
+
|
| 179 |
+
Args:
|
| 180 |
+
lines (sequence of arrays): Contour line data as returned by
|
| 181 |
+
:meth:`~.ContourGenerator.lines`.
|
| 182 |
+
line_type (LineType or str): Type of :meth:`~.ContourGenerator.lines` data as returned
|
| 183 |
+
by :attr:`~.ContourGenerator.line_type`, or string equivalent.
|
| 184 |
+
ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
|
| 185 |
+
color (str, optional): Color to plot lines. May be a string color or the letter ``"C"``
|
| 186 |
+
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
|
| 187 |
+
``tab10`` colormap. Default ``"C0"``.
|
| 188 |
+
alpha (float, optional): Opacity to plot lines with, default ``1.0``.
|
| 189 |
+
linewidth (float, optional): Width of lines, default ``1``.
|
| 190 |
+
"""
|
| 191 |
+
line_type = as_line_type(line_type)
|
| 192 |
+
ax = self._get_ax(ax)
|
| 193 |
+
paths = lines_to_mpl_paths(lines, line_type)
|
| 194 |
+
collection = mcollections.PathCollection(
|
| 195 |
+
paths, facecolors="none", edgecolors=color, lw=linewidth, alpha=alpha)
|
| 196 |
+
ax.add_collection(collection)
|
| 197 |
+
ax._need_autoscale = True # type: ignore[attr-defined]
|
| 198 |
+
|
| 199 |
+
def mask(
|
| 200 |
+
self,
|
| 201 |
+
x: ArrayLike,
|
| 202 |
+
y: ArrayLike,
|
| 203 |
+
z: ArrayLike | np.ma.MaskedArray[Any, Any],
|
| 204 |
+
ax: Axes | int = 0,
|
| 205 |
+
color: str = "black",
|
| 206 |
+
) -> None:
|
| 207 |
+
"""Plot masked out grid points as circles on a single Axes.
|
| 208 |
+
|
| 209 |
+
Args:
|
| 210 |
+
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
|
| 211 |
+
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
|
| 212 |
+
z (masked array of shape (ny, nx): z-values.
|
| 213 |
+
ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
|
| 214 |
+
color (str, optional): Circle color, default ``"black"``.
|
| 215 |
+
"""
|
| 216 |
+
mask = np.ma.getmask(z) # type: ignore[no-untyped-call]
|
| 217 |
+
if mask is np.ma.nomask:
|
| 218 |
+
return
|
| 219 |
+
ax = self._get_ax(ax)
|
| 220 |
+
x, y = self._grid_as_2d(x, y)
|
| 221 |
+
ax.plot(x[mask], y[mask], "o", c=color)
|
| 222 |
+
|
| 223 |
+
def save(self, filename: str, transparent: bool = False) -> None:
|
| 224 |
+
"""Save plots to SVG or PNG file.
|
| 225 |
+
|
| 226 |
+
Args:
|
| 227 |
+
filename (str): Filename to save to.
|
| 228 |
+
transparent (bool, optional): Whether background should be transparent, default
|
| 229 |
+
``False``.
|
| 230 |
+
"""
|
| 231 |
+
self._autoscale()
|
| 232 |
+
self._fig.savefig(filename, transparent=transparent)
|
| 233 |
+
|
| 234 |
+
def save_to_buffer(self) -> io.BytesIO:
|
| 235 |
+
"""Save plots to an ``io.BytesIO`` buffer.
|
| 236 |
+
|
| 237 |
+
Return:
|
| 238 |
+
BytesIO: PNG image buffer.
|
| 239 |
+
"""
|
| 240 |
+
self._autoscale()
|
| 241 |
+
buf = io.BytesIO()
|
| 242 |
+
self._fig.savefig(buf, format="png")
|
| 243 |
+
buf.seek(0)
|
| 244 |
+
return buf
|
| 245 |
+
|
| 246 |
+
def show(self) -> None:
|
| 247 |
+
"""Show plots in an interactive window, in the usual Matplotlib manner.
|
| 248 |
+
"""
|
| 249 |
+
self._autoscale()
|
| 250 |
+
plt.show()
|
| 251 |
+
|
| 252 |
+
def title(self, title: str, ax: Axes | int = 0, color: str | None = None) -> None:
|
| 253 |
+
"""Set the title of a single Axes.
|
| 254 |
+
|
| 255 |
+
Args:
|
| 256 |
+
title (str): Title text.
|
| 257 |
+
ax (int or Matplotlib Axes, optional): Which Axes to set the title of, default ``0``.
|
| 258 |
+
color (str, optional): Color to set title. May be a string color or the letter ``"C"``
|
| 259 |
+
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
|
| 260 |
+
``tab10`` colormap. Default is ``None`` which uses Matplotlib's default title color
|
| 261 |
+
that depends on the stylesheet in use.
|
| 262 |
+
"""
|
| 263 |
+
if color:
|
| 264 |
+
self._get_ax(ax).set_title(title, color=color)
|
| 265 |
+
else:
|
| 266 |
+
self._get_ax(ax).set_title(title)
|
| 267 |
+
|
| 268 |
+
def z_values(
|
| 269 |
+
self,
|
| 270 |
+
x: ArrayLike,
|
| 271 |
+
y: ArrayLike,
|
| 272 |
+
z: ArrayLike,
|
| 273 |
+
ax: Axes | int = 0,
|
| 274 |
+
color: str = "green",
|
| 275 |
+
fmt: str = ".1f",
|
| 276 |
+
quad_as_tri: bool = False,
|
| 277 |
+
) -> None:
|
| 278 |
+
"""Show ``z`` values on a single Axes.
|
| 279 |
+
|
| 280 |
+
Args:
|
| 281 |
+
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
|
| 282 |
+
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
|
| 283 |
+
z (array-like of shape (ny, nx): z-values.
|
| 284 |
+
ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
|
| 285 |
+
color (str, optional): Color of added text. May be a string color or the letter ``"C"``
|
| 286 |
+
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
|
| 287 |
+
``tab10`` colormap. Default ``"green"``.
|
| 288 |
+
fmt (str, optional): Format to display z-values, default ``".1f"``.
|
| 289 |
+
quad_as_tri (bool, optional): Whether to show z-values at the ``quad_as_tri`` centers
|
| 290 |
+
of quads.
|
| 291 |
+
|
| 292 |
+
Warning:
|
| 293 |
+
``quad_as_tri=True`` shows z-values for all quads, even if masked.
|
| 294 |
+
"""
|
| 295 |
+
ax = self._get_ax(ax)
|
| 296 |
+
x, y = self._grid_as_2d(x, y)
|
| 297 |
+
z = np.asarray(z)
|
| 298 |
+
ny, nx = z.shape
|
| 299 |
+
for j in range(ny):
|
| 300 |
+
for i in range(nx):
|
| 301 |
+
ax.text(x[j, i], y[j, i], f"{z[j, i]:{fmt}}", ha="center", va="center",
|
| 302 |
+
color=color, clip_on=True)
|
| 303 |
+
if quad_as_tri:
|
| 304 |
+
for j in range(ny-1):
|
| 305 |
+
for i in range(nx-1):
|
| 306 |
+
xx = np.mean(x[j:j+2, i:i+2], dtype=np.float64)
|
| 307 |
+
yy = np.mean(y[j:j+2, i:i+2], dtype=np.float64)
|
| 308 |
+
zz = np.mean(z[j:j+2, i:i+2])
|
| 309 |
+
ax.text(xx, yy, f"{zz:{fmt}}", ha="center", va="center", color=color,
|
| 310 |
+
clip_on=True)
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
class MplTestRenderer(MplRenderer):
|
| 314 |
+
"""Test renderer implemented using Matplotlib.
|
| 315 |
+
|
| 316 |
+
No whitespace around plots and no spines/ticks displayed.
|
| 317 |
+
Uses Agg backend, so can only save to file/buffer, cannot call ``show()``.
|
| 318 |
+
"""
|
| 319 |
+
def __init__(
|
| 320 |
+
self,
|
| 321 |
+
nrows: int = 1,
|
| 322 |
+
ncols: int = 1,
|
| 323 |
+
figsize: tuple[float, float] = (9, 9),
|
| 324 |
+
) -> None:
|
| 325 |
+
gridspec = {
|
| 326 |
+
"left": 0.01,
|
| 327 |
+
"right": 0.99,
|
| 328 |
+
"top": 0.99,
|
| 329 |
+
"bottom": 0.01,
|
| 330 |
+
"wspace": 0.01,
|
| 331 |
+
"hspace": 0.01,
|
| 332 |
+
}
|
| 333 |
+
super().__init__(
|
| 334 |
+
nrows, ncols, figsize, show_frame=True, backend="Agg", gridspec_kw=gridspec,
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
for ax in self._axes:
|
| 338 |
+
ax.set_xmargin(0.0)
|
| 339 |
+
ax.set_ymargin(0.0)
|
| 340 |
+
ax.set_xticks([])
|
| 341 |
+
ax.set_yticks([])
|
| 342 |
+
|
| 343 |
+
self._want_tight = False
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
class MplDebugRenderer(MplRenderer):
|
| 347 |
+
"""Debug renderer implemented using Matplotlib.
|
| 348 |
+
|
| 349 |
+
Extends ``MplRenderer`` to add extra information to help in debugging such as markers, arrows,
|
| 350 |
+
text, etc.
|
| 351 |
+
"""
|
| 352 |
+
def __init__(
|
| 353 |
+
self,
|
| 354 |
+
nrows: int = 1,
|
| 355 |
+
ncols: int = 1,
|
| 356 |
+
figsize: tuple[float, float] = (9, 9),
|
| 357 |
+
show_frame: bool = True,
|
| 358 |
+
) -> None:
|
| 359 |
+
super().__init__(nrows, ncols, figsize, show_frame)
|
| 360 |
+
|
| 361 |
+
def _arrow(
|
| 362 |
+
self,
|
| 363 |
+
ax: Axes,
|
| 364 |
+
line_start: cpy.CoordinateArray,
|
| 365 |
+
line_end: cpy.CoordinateArray,
|
| 366 |
+
color: str,
|
| 367 |
+
alpha: float,
|
| 368 |
+
arrow_size: float,
|
| 369 |
+
) -> None:
|
| 370 |
+
mid = 0.5*(line_start + line_end)
|
| 371 |
+
along = line_end - line_start
|
| 372 |
+
along /= np.sqrt(np.dot(along, along)) # Unit vector.
|
| 373 |
+
right = np.asarray((along[1], -along[0]))
|
| 374 |
+
arrow = np.stack((
|
| 375 |
+
mid - (along*0.5 - right)*arrow_size,
|
| 376 |
+
mid + along*0.5*arrow_size,
|
| 377 |
+
mid - (along*0.5 + right)*arrow_size,
|
| 378 |
+
))
|
| 379 |
+
ax.plot(arrow[:, 0], arrow[:, 1], "-", c=color, alpha=alpha)
|
| 380 |
+
|
| 381 |
+
def filled(
|
| 382 |
+
self,
|
| 383 |
+
filled: cpy.FillReturn,
|
| 384 |
+
fill_type: FillType | str,
|
| 385 |
+
ax: Axes | int = 0,
|
| 386 |
+
color: str = "C1",
|
| 387 |
+
alpha: float = 0.7,
|
| 388 |
+
line_color: str = "C0",
|
| 389 |
+
line_alpha: float = 0.7,
|
| 390 |
+
point_color: str = "C0",
|
| 391 |
+
start_point_color: str = "red",
|
| 392 |
+
arrow_size: float = 0.1,
|
| 393 |
+
) -> None:
|
| 394 |
+
fill_type = as_fill_type(fill_type)
|
| 395 |
+
super().filled(filled, fill_type, ax, color, alpha)
|
| 396 |
+
|
| 397 |
+
if line_color is None and point_color is None:
|
| 398 |
+
return
|
| 399 |
+
|
| 400 |
+
ax = self._get_ax(ax)
|
| 401 |
+
filled = convert_filled(filled, fill_type, FillType.ChunkCombinedOffset)
|
| 402 |
+
|
| 403 |
+
# Lines.
|
| 404 |
+
if line_color is not None:
|
| 405 |
+
for points, offsets in zip(*filled):
|
| 406 |
+
if points is None:
|
| 407 |
+
continue
|
| 408 |
+
for start, end in pairwise(offsets):
|
| 409 |
+
xys = points[start:end]
|
| 410 |
+
ax.plot(xys[:, 0], xys[:, 1], c=line_color, alpha=line_alpha)
|
| 411 |
+
|
| 412 |
+
if arrow_size > 0.0:
|
| 413 |
+
n = len(xys)
|
| 414 |
+
for i in range(n-1):
|
| 415 |
+
self._arrow(ax, xys[i], xys[i+1], line_color, line_alpha, arrow_size)
|
| 416 |
+
|
| 417 |
+
# Points.
|
| 418 |
+
if point_color is not None:
|
| 419 |
+
for points, offsets in zip(*filled):
|
| 420 |
+
if points is None:
|
| 421 |
+
continue
|
| 422 |
+
mask = np.ones(offsets[-1], dtype=bool)
|
| 423 |
+
mask[offsets[1:]-1] = False # Exclude end points.
|
| 424 |
+
if start_point_color is not None:
|
| 425 |
+
start_indices = offsets[:-1]
|
| 426 |
+
mask[start_indices] = False # Exclude start points.
|
| 427 |
+
ax.plot(
|
| 428 |
+
points[:, 0][mask], points[:, 1][mask], "o", c=point_color, alpha=line_alpha)
|
| 429 |
+
|
| 430 |
+
if start_point_color is not None:
|
| 431 |
+
ax.plot(points[:, 0][start_indices], points[:, 1][start_indices], "o",
|
| 432 |
+
c=start_point_color, alpha=line_alpha)
|
| 433 |
+
|
| 434 |
+
def lines(
|
| 435 |
+
self,
|
| 436 |
+
lines: cpy.LineReturn,
|
| 437 |
+
line_type: LineType | str,
|
| 438 |
+
ax: Axes | int = 0,
|
| 439 |
+
color: str = "C0",
|
| 440 |
+
alpha: float = 1.0,
|
| 441 |
+
linewidth: float = 1,
|
| 442 |
+
point_color: str = "C0",
|
| 443 |
+
start_point_color: str = "red",
|
| 444 |
+
arrow_size: float = 0.1,
|
| 445 |
+
) -> None:
|
| 446 |
+
line_type = as_line_type(line_type)
|
| 447 |
+
super().lines(lines, line_type, ax, color, alpha, linewidth)
|
| 448 |
+
|
| 449 |
+
if arrow_size == 0.0 and point_color is None:
|
| 450 |
+
return
|
| 451 |
+
|
| 452 |
+
ax = self._get_ax(ax)
|
| 453 |
+
separate_lines = convert_lines(lines, line_type, LineType.Separate)
|
| 454 |
+
if TYPE_CHECKING:
|
| 455 |
+
separate_lines = cast(cpy.LineReturn_Separate, separate_lines)
|
| 456 |
+
|
| 457 |
+
if arrow_size > 0.0:
|
| 458 |
+
for line in separate_lines:
|
| 459 |
+
for i in range(len(line)-1):
|
| 460 |
+
self._arrow(ax, line[i], line[i+1], color, alpha, arrow_size)
|
| 461 |
+
|
| 462 |
+
if point_color is not None:
|
| 463 |
+
for line in separate_lines:
|
| 464 |
+
start_index = 0
|
| 465 |
+
end_index = len(line)
|
| 466 |
+
if start_point_color is not None:
|
| 467 |
+
ax.plot(line[0, 0], line[0, 1], "o", c=start_point_color, alpha=alpha)
|
| 468 |
+
start_index = 1
|
| 469 |
+
if line[0][0] == line[-1][0] and line[0][1] == line[-1][1]:
|
| 470 |
+
end_index -= 1
|
| 471 |
+
ax.plot(line[start_index:end_index, 0], line[start_index:end_index, 1], "o",
|
| 472 |
+
c=color, alpha=alpha)
|
| 473 |
+
|
| 474 |
+
def point_numbers(
|
| 475 |
+
self,
|
| 476 |
+
x: ArrayLike,
|
| 477 |
+
y: ArrayLike,
|
| 478 |
+
z: ArrayLike,
|
| 479 |
+
ax: Axes | int = 0,
|
| 480 |
+
color: str = "red",
|
| 481 |
+
) -> None:
|
| 482 |
+
ax = self._get_ax(ax)
|
| 483 |
+
x, y = self._grid_as_2d(x, y)
|
| 484 |
+
z = np.asarray(z)
|
| 485 |
+
ny, nx = z.shape
|
| 486 |
+
for j in range(ny):
|
| 487 |
+
for i in range(nx):
|
| 488 |
+
quad = i + j*nx
|
| 489 |
+
ax.text(x[j, i], y[j, i], str(quad), ha="right", va="top", color=color,
|
| 490 |
+
clip_on=True)
|
| 491 |
+
|
| 492 |
+
def quad_numbers(
|
| 493 |
+
self,
|
| 494 |
+
x: ArrayLike,
|
| 495 |
+
y: ArrayLike,
|
| 496 |
+
z: ArrayLike,
|
| 497 |
+
ax: Axes | int = 0,
|
| 498 |
+
color: str = "blue",
|
| 499 |
+
) -> None:
|
| 500 |
+
ax = self._get_ax(ax)
|
| 501 |
+
x, y = self._grid_as_2d(x, y)
|
| 502 |
+
z = np.asarray(z)
|
| 503 |
+
ny, nx = z.shape
|
| 504 |
+
for j in range(1, ny):
|
| 505 |
+
for i in range(1, nx):
|
| 506 |
+
quad = i + j*nx
|
| 507 |
+
xmid = x[j-1:j+1, i-1:i+1].mean()
|
| 508 |
+
ymid = y[j-1:j+1, i-1:i+1].mean()
|
| 509 |
+
ax.text(xmid, ymid, str(quad), ha="center", va="center", color=color, clip_on=True)
|
| 510 |
+
|
| 511 |
+
def z_levels(
|
| 512 |
+
self,
|
| 513 |
+
x: ArrayLike,
|
| 514 |
+
y: ArrayLike,
|
| 515 |
+
z: ArrayLike,
|
| 516 |
+
lower_level: float,
|
| 517 |
+
upper_level: float | None = None,
|
| 518 |
+
ax: Axes | int = 0,
|
| 519 |
+
color: str = "green",
|
| 520 |
+
) -> None:
|
| 521 |
+
ax = self._get_ax(ax)
|
| 522 |
+
x, y = self._grid_as_2d(x, y)
|
| 523 |
+
z = np.asarray(z)
|
| 524 |
+
ny, nx = z.shape
|
| 525 |
+
for j in range(ny):
|
| 526 |
+
for i in range(nx):
|
| 527 |
+
zz = z[j, i]
|
| 528 |
+
if upper_level is not None and zz > upper_level:
|
| 529 |
+
z_level = 2
|
| 530 |
+
elif zz > lower_level:
|
| 531 |
+
z_level = 1
|
| 532 |
+
else:
|
| 533 |
+
z_level = 0
|
| 534 |
+
ax.text(x[j, i], y[j, i], str(z_level), ha="left", va="bottom", color=color,
|
| 535 |
+
clip_on=True)
|
venv/lib/python3.10/site-packages/contourpy/util/mpl_util.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from itertools import pairwise
|
| 4 |
+
from typing import TYPE_CHECKING, cast
|
| 5 |
+
|
| 6 |
+
import matplotlib.path as mpath
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from contourpy import FillType, LineType
|
| 10 |
+
from contourpy.array import codes_from_offsets
|
| 11 |
+
|
| 12 |
+
if TYPE_CHECKING:
|
| 13 |
+
from contourpy._contourpy import FillReturn, LineReturn, LineReturn_Separate
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def filled_to_mpl_paths(filled: FillReturn, fill_type: FillType) -> list[mpath.Path]:
|
| 17 |
+
if fill_type in (FillType.OuterCode, FillType.ChunkCombinedCode):
|
| 18 |
+
paths = [mpath.Path(points, codes) for points, codes in zip(*filled) if points is not None]
|
| 19 |
+
elif fill_type in (FillType.OuterOffset, FillType.ChunkCombinedOffset):
|
| 20 |
+
paths = [mpath.Path(points, codes_from_offsets(offsets))
|
| 21 |
+
for points, offsets in zip(*filled) if points is not None]
|
| 22 |
+
elif fill_type == FillType.ChunkCombinedCodeOffset:
|
| 23 |
+
paths = []
|
| 24 |
+
for points, codes, outer_offsets in zip(*filled):
|
| 25 |
+
if points is None:
|
| 26 |
+
continue
|
| 27 |
+
points = np.split(points, outer_offsets[1:-1])
|
| 28 |
+
codes = np.split(codes, outer_offsets[1:-1])
|
| 29 |
+
paths += [mpath.Path(p, c) for p, c in zip(points, codes)]
|
| 30 |
+
elif fill_type == FillType.ChunkCombinedOffsetOffset:
|
| 31 |
+
paths = []
|
| 32 |
+
for points, offsets, outer_offsets in zip(*filled):
|
| 33 |
+
if points is None:
|
| 34 |
+
continue
|
| 35 |
+
for i in range(len(outer_offsets)-1):
|
| 36 |
+
offs = offsets[outer_offsets[i]:outer_offsets[i+1]+1]
|
| 37 |
+
pts = points[offs[0]:offs[-1]]
|
| 38 |
+
paths += [mpath.Path(pts, codes_from_offsets(offs - offs[0]))]
|
| 39 |
+
else:
|
| 40 |
+
raise RuntimeError(f"Conversion of FillType {fill_type} to MPL Paths is not implemented")
|
| 41 |
+
return paths
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def lines_to_mpl_paths(lines: LineReturn, line_type: LineType) -> list[mpath.Path]:
|
| 45 |
+
if line_type == LineType.Separate:
|
| 46 |
+
if TYPE_CHECKING:
|
| 47 |
+
lines = cast(LineReturn_Separate, lines)
|
| 48 |
+
paths = []
|
| 49 |
+
for line in lines:
|
| 50 |
+
# Drawing as Paths so that they can be closed correctly.
|
| 51 |
+
closed = line[0, 0] == line[-1, 0] and line[0, 1] == line[-1, 1]
|
| 52 |
+
paths.append(mpath.Path(line, closed=closed))
|
| 53 |
+
elif line_type in (LineType.SeparateCode, LineType.ChunkCombinedCode):
|
| 54 |
+
paths = [mpath.Path(points, codes) for points, codes in zip(*lines) if points is not None]
|
| 55 |
+
elif line_type == LineType.ChunkCombinedOffset:
|
| 56 |
+
paths = []
|
| 57 |
+
for points, offsets in zip(*lines):
|
| 58 |
+
if points is None:
|
| 59 |
+
continue
|
| 60 |
+
for i in range(len(offsets)-1):
|
| 61 |
+
line = points[offsets[i]:offsets[i+1]]
|
| 62 |
+
closed = line[0, 0] == line[-1, 0] and line[0, 1] == line[-1, 1]
|
| 63 |
+
paths.append(mpath.Path(line, closed=closed))
|
| 64 |
+
elif line_type == LineType.ChunkCombinedNan:
|
| 65 |
+
paths = []
|
| 66 |
+
for points in lines[0]:
|
| 67 |
+
if points is None:
|
| 68 |
+
continue
|
| 69 |
+
nan_offsets = np.nonzero(np.isnan(points[:, 0]))[0]
|
| 70 |
+
nan_offsets = np.concatenate([[-1], nan_offsets, [len(points)]])
|
| 71 |
+
for s, e in pairwise(nan_offsets):
|
| 72 |
+
line = points[s+1:e]
|
| 73 |
+
closed = line[0, 0] == line[-1, 0] and line[0, 1] == line[-1, 1]
|
| 74 |
+
paths.append(mpath.Path(line, closed=closed))
|
| 75 |
+
else:
|
| 76 |
+
raise RuntimeError(f"Conversion of LineType {line_type} to MPL Paths is not implemented")
|
| 77 |
+
return paths
|
venv/lib/python3.10/site-packages/contourpy/util/renderer.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
from typing import TYPE_CHECKING, Any
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
if TYPE_CHECKING:
|
| 9 |
+
import io
|
| 10 |
+
|
| 11 |
+
from numpy.typing import ArrayLike
|
| 12 |
+
|
| 13 |
+
from contourpy._contourpy import CoordinateArray, FillReturn, FillType, LineReturn, LineType
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class Renderer(ABC):
|
| 17 |
+
"""Abstract base class for renderers."""
|
| 18 |
+
|
| 19 |
+
def _grid_as_2d(self, x: ArrayLike, y: ArrayLike) -> tuple[CoordinateArray, CoordinateArray]:
|
| 20 |
+
x = np.asarray(x)
|
| 21 |
+
y = np.asarray(y)
|
| 22 |
+
if x.ndim == 1:
|
| 23 |
+
x, y = np.meshgrid(x, y)
|
| 24 |
+
return x, y
|
| 25 |
+
|
| 26 |
+
@abstractmethod
|
| 27 |
+
def filled(
|
| 28 |
+
self,
|
| 29 |
+
filled: FillReturn,
|
| 30 |
+
fill_type: FillType | str,
|
| 31 |
+
ax: Any = 0,
|
| 32 |
+
color: str = "C0",
|
| 33 |
+
alpha: float = 0.7,
|
| 34 |
+
) -> None:
|
| 35 |
+
pass
|
| 36 |
+
|
| 37 |
+
@abstractmethod
|
| 38 |
+
def grid(
|
| 39 |
+
self,
|
| 40 |
+
x: ArrayLike,
|
| 41 |
+
y: ArrayLike,
|
| 42 |
+
ax: Any = 0,
|
| 43 |
+
color: str = "black",
|
| 44 |
+
alpha: float = 0.1,
|
| 45 |
+
point_color: str | None = None,
|
| 46 |
+
quad_as_tri_alpha: float = 0,
|
| 47 |
+
) -> None:
|
| 48 |
+
pass
|
| 49 |
+
|
| 50 |
+
@abstractmethod
|
| 51 |
+
def lines(
|
| 52 |
+
self,
|
| 53 |
+
lines: LineReturn,
|
| 54 |
+
line_type: LineType | str,
|
| 55 |
+
ax: Any = 0,
|
| 56 |
+
color: str = "C0",
|
| 57 |
+
alpha: float = 1.0,
|
| 58 |
+
linewidth: float = 1,
|
| 59 |
+
) -> None:
|
| 60 |
+
pass
|
| 61 |
+
|
| 62 |
+
@abstractmethod
|
| 63 |
+
def mask(
|
| 64 |
+
self,
|
| 65 |
+
x: ArrayLike,
|
| 66 |
+
y: ArrayLike,
|
| 67 |
+
z: ArrayLike | np.ma.MaskedArray[Any, Any],
|
| 68 |
+
ax: Any = 0,
|
| 69 |
+
color: str = "black",
|
| 70 |
+
) -> None:
|
| 71 |
+
pass
|
| 72 |
+
|
| 73 |
+
def multi_filled(
|
| 74 |
+
self,
|
| 75 |
+
multi_filled: list[FillReturn],
|
| 76 |
+
fill_type: FillType | str,
|
| 77 |
+
ax: Any = 0,
|
| 78 |
+
color: str | None = None,
|
| 79 |
+
**kwargs: Any,
|
| 80 |
+
) -> None:
|
| 81 |
+
"""Plot multiple sets of filled contours on a single axes.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
multi_filled (list of filled contour arrays): Multiple filled contour sets as returned
|
| 85 |
+
by :meth:`.ContourGenerator.multi_filled`.
|
| 86 |
+
fill_type (FillType or str): Type of filled data as returned by
|
| 87 |
+
:attr:`~.ContourGenerator.fill_type`, or string equivalent.
|
| 88 |
+
ax (int or Renderer-specific axes or figure object, optional): Which axes to plot on,
|
| 89 |
+
default ``0``.
|
| 90 |
+
color (str or None, optional): If a string color then this same color is used for all
|
| 91 |
+
filled contours. If ``None``, the default, then the filled contour sets use colors
|
| 92 |
+
from the ``tab10`` colormap in order, wrapping around to the beginning if more than
|
| 93 |
+
10 sets of filled contours are rendered.
|
| 94 |
+
kwargs: All other keyword argument are passed on to
|
| 95 |
+
:meth:`.Renderer.filled` unchanged.
|
| 96 |
+
|
| 97 |
+
.. versionadded:: 1.3.0
|
| 98 |
+
"""
|
| 99 |
+
if color is not None:
|
| 100 |
+
kwargs["color"] = color
|
| 101 |
+
for i, filled in enumerate(multi_filled):
|
| 102 |
+
if color is None:
|
| 103 |
+
kwargs["color"] = f"C{i % 10}"
|
| 104 |
+
self.filled(filled, fill_type, ax, **kwargs)
|
| 105 |
+
|
| 106 |
+
def multi_lines(
|
| 107 |
+
self,
|
| 108 |
+
multi_lines: list[LineReturn],
|
| 109 |
+
line_type: LineType | str,
|
| 110 |
+
ax: Any = 0,
|
| 111 |
+
color: str | None = None,
|
| 112 |
+
**kwargs: Any,
|
| 113 |
+
) -> None:
|
| 114 |
+
"""Plot multiple sets of contour lines on a single axes.
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
multi_lines (list of contour line arrays): Multiple contour line sets as returned by
|
| 118 |
+
:meth:`.ContourGenerator.multi_lines`.
|
| 119 |
+
line_type (LineType or str): Type of line data as returned by
|
| 120 |
+
:attr:`~.ContourGenerator.line_type`, or string equivalent.
|
| 121 |
+
ax (int or Renderer-specific axes or figure object, optional): Which axes to plot on,
|
| 122 |
+
default ``0``.
|
| 123 |
+
color (str or None, optional): If a string color then this same color is used for all
|
| 124 |
+
lines. If ``None``, the default, then the line sets use colors from the ``tab10``
|
| 125 |
+
colormap in order, wrapping around to the beginning if more than 10 sets of lines
|
| 126 |
+
are rendered.
|
| 127 |
+
kwargs: All other keyword argument are passed on to
|
| 128 |
+
:meth:`Renderer.lines` unchanged.
|
| 129 |
+
|
| 130 |
+
.. versionadded:: 1.3.0
|
| 131 |
+
"""
|
| 132 |
+
if color is not None:
|
| 133 |
+
kwargs["color"] = color
|
| 134 |
+
for i, lines in enumerate(multi_lines):
|
| 135 |
+
if color is None:
|
| 136 |
+
kwargs["color"] = f"C{i % 10}"
|
| 137 |
+
self.lines(lines, line_type, ax, **kwargs)
|
| 138 |
+
|
| 139 |
+
@abstractmethod
|
| 140 |
+
def save(self, filename: str, transparent: bool = False) -> None:
|
| 141 |
+
pass
|
| 142 |
+
|
| 143 |
+
@abstractmethod
|
| 144 |
+
def save_to_buffer(self) -> io.BytesIO:
|
| 145 |
+
pass
|
| 146 |
+
|
| 147 |
+
@abstractmethod
|
| 148 |
+
def show(self) -> None:
|
| 149 |
+
pass
|
| 150 |
+
|
| 151 |
+
@abstractmethod
|
| 152 |
+
def title(self, title: str, ax: Any = 0, color: str | None = None) -> None:
|
| 153 |
+
pass
|
| 154 |
+
|
| 155 |
+
@abstractmethod
|
| 156 |
+
def z_values(
|
| 157 |
+
self,
|
| 158 |
+
x: ArrayLike,
|
| 159 |
+
y: ArrayLike,
|
| 160 |
+
z: ArrayLike,
|
| 161 |
+
ax: Any = 0,
|
| 162 |
+
color: str = "green",
|
| 163 |
+
fmt: str = ".1f",
|
| 164 |
+
quad_as_tri: bool = False,
|
| 165 |
+
) -> None:
|
| 166 |
+
pass
|
venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/METADATA
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: filelock
|
| 3 |
+
Version: 3.20.0
|
| 4 |
+
Summary: A platform independent file lock.
|
| 5 |
+
Project-URL: Documentation, https://py-filelock.readthedocs.io
|
| 6 |
+
Project-URL: Homepage, https://github.com/tox-dev/py-filelock
|
| 7 |
+
Project-URL: Source, https://github.com/tox-dev/py-filelock
|
| 8 |
+
Project-URL: Tracker, https://github.com/tox-dev/py-filelock/issues
|
| 9 |
+
Maintainer-email: Bernát Gábor <gaborjbernat@gmail.com>
|
| 10 |
+
License-Expression: Unlicense
|
| 11 |
+
License-File: LICENSE
|
| 12 |
+
Keywords: application,cache,directory,log,user
|
| 13 |
+
Classifier: Development Status :: 5 - Production/Stable
|
| 14 |
+
Classifier: Intended Audience :: Developers
|
| 15 |
+
Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
|
| 16 |
+
Classifier: Operating System :: OS Independent
|
| 17 |
+
Classifier: Programming Language :: Python
|
| 18 |
+
Classifier: Programming Language :: Python :: 3 :: Only
|
| 19 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 21 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 22 |
+
Classifier: Programming Language :: Python :: 3.13
|
| 23 |
+
Classifier: Programming Language :: Python :: 3.14
|
| 24 |
+
Classifier: Topic :: Internet
|
| 25 |
+
Classifier: Topic :: Software Development :: Libraries
|
| 26 |
+
Classifier: Topic :: System
|
| 27 |
+
Requires-Python: >=3.10
|
| 28 |
+
Description-Content-Type: text/markdown
|
| 29 |
+
|
| 30 |
+
# filelock
|
| 31 |
+
|
| 32 |
+
[](https://pypi.org/project/filelock/)
|
| 33 |
+
[](https://pypi.org/project/filelock/)
|
| 35 |
+
[](https://py-filelock.readthedocs.io/en/latest/?badge=latest)
|
| 37 |
+
[](https://github.com/psf/black)
|
| 39 |
+
[](https://pepy.tech/project/filelock)
|
| 40 |
+
[](https://github.com/tox-dev/py-filelock/actions/workflows/check.yaml)
|
| 41 |
+
|
| 42 |
+
For more information checkout the [official documentation](https://py-filelock.readthedocs.io/en/latest/index.html).
|
venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/RECORD
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
filelock-3.20.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
filelock-3.20.0.dist-info/METADATA,sha256=gIghqdcbGNywxw52pN02_a9OxFqzhjA8v-9GsDWtNog,2110
|
| 3 |
+
filelock-3.20.0.dist-info/RECORD,,
|
| 4 |
+
filelock-3.20.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 5 |
+
filelock-3.20.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
| 6 |
+
filelock-3.20.0.dist-info/licenses/LICENSE,sha256=iNm062BXnBkew5HKBMFhMFctfu3EqG2qWL8oxuFMm80,1210
|
| 7 |
+
filelock/__init__.py,sha256=_t_-OAGXo_qyPa9lNQ1YnzVYEvSW3I0onPqzpomsVVg,1769
|
| 8 |
+
filelock/__pycache__/__init__.cpython-310.pyc,,
|
| 9 |
+
filelock/__pycache__/_api.cpython-310.pyc,,
|
| 10 |
+
filelock/__pycache__/_error.cpython-310.pyc,,
|
| 11 |
+
filelock/__pycache__/_soft.cpython-310.pyc,,
|
| 12 |
+
filelock/__pycache__/_unix.cpython-310.pyc,,
|
| 13 |
+
filelock/__pycache__/_util.cpython-310.pyc,,
|
| 14 |
+
filelock/__pycache__/_windows.cpython-310.pyc,,
|
| 15 |
+
filelock/__pycache__/asyncio.cpython-310.pyc,,
|
| 16 |
+
filelock/__pycache__/version.cpython-310.pyc,,
|
| 17 |
+
filelock/_api.py,sha256=2aATBeJ3-jtMj5OSm7EE539iNaTBsf13KXtcBMoi8oM,14545
|
| 18 |
+
filelock/_error.py,sha256=-5jMcjTu60YAvAO1UbqDD1GIEjVkwr8xCFwDBtMeYDg,787
|
| 19 |
+
filelock/_soft.py,sha256=haqtc_TB_KJbYv2a8iuEAclKuM4fMG1vTcp28sK919c,1711
|
| 20 |
+
filelock/_unix.py,sha256=eGOs4gDgZ-5fGnJUz-OkJDeZkAMzgvYcD8hVD6XH7e4,2351
|
| 21 |
+
filelock/_util.py,sha256=QHBoNFIYfbAThhotH3Q8E2acFc84wpG49-T-uu017ZE,1715
|
| 22 |
+
filelock/_windows.py,sha256=8k4XIBl_zZVfGC2gz0kEr8DZBvpNa8wdU9qeM1YrBb8,2179
|
| 23 |
+
filelock/asyncio.py,sha256=dSLe6XZSECFOgsVpcQUSh5Y5zAHxHGPu_tfpPX9I45k,12514
|
| 24 |
+
filelock/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 25 |
+
filelock/version.py,sha256=AW5MeEjK4TaQWWJrGb_AlBw8PlmFoIcn7GodG_AVSOM,706
|
venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/REQUESTED
ADDED
|
File without changes
|
venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: hatchling 1.27.0
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
venv/lib/python3.10/site-packages/filelock-3.20.0.dist-info/licenses/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This is free and unencumbered software released into the public domain.
|
| 2 |
+
|
| 3 |
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
| 4 |
+
distribute this software, either in source code form or as a compiled
|
| 5 |
+
binary, for any purpose, commercial or non-commercial, and by any
|
| 6 |
+
means.
|
| 7 |
+
|
| 8 |
+
In jurisdictions that recognize copyright laws, the author or authors
|
| 9 |
+
of this software dedicate any and all copyright interest in the
|
| 10 |
+
software to the public domain. We make this dedication for the benefit
|
| 11 |
+
of the public at large and to the detriment of our heirs and
|
| 12 |
+
successors. We intend this dedication to be an overt act of
|
| 13 |
+
relinquishment in perpetuity of all present and future rights to this
|
| 14 |
+
software under copyright law.
|
| 15 |
+
|
| 16 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
| 17 |
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
| 18 |
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
| 19 |
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
| 20 |
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
| 21 |
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
| 22 |
+
OTHER DEALINGS IN THE SOFTWARE.
|
| 23 |
+
|
| 24 |
+
For more information, please refer to <http://unlicense.org>
|
venv/lib/python3.10/site-packages/fontTools/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from fontTools.misc.loggingTools import configLogger
|
| 3 |
+
|
| 4 |
+
log = logging.getLogger(__name__)
|
| 5 |
+
|
| 6 |
+
version = __version__ = "4.60.1"
|
| 7 |
+
|
| 8 |
+
__all__ = ["version", "log", "configLogger"]
|
venv/lib/python3.10/site-packages/fontTools/__main__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def main(args=None):
|
| 5 |
+
if args is None:
|
| 6 |
+
args = sys.argv[1:]
|
| 7 |
+
|
| 8 |
+
# TODO Handle library-wide options. Eg.:
|
| 9 |
+
# --unicodedata
|
| 10 |
+
# --verbose / other logging stuff
|
| 11 |
+
|
| 12 |
+
# TODO Allow a way to run arbitrary modules? Useful for setting
|
| 13 |
+
# library-wide options and calling another library. Eg.:
|
| 14 |
+
#
|
| 15 |
+
# $ fonttools --unicodedata=... fontmake ...
|
| 16 |
+
#
|
| 17 |
+
# This allows for a git-like command where thirdparty commands
|
| 18 |
+
# can be added. Should we just try importing the fonttools
|
| 19 |
+
# module first and try without if it fails?
|
| 20 |
+
|
| 21 |
+
if len(sys.argv) < 2:
|
| 22 |
+
sys.argv.append("help")
|
| 23 |
+
if sys.argv[1] == "-h" or sys.argv[1] == "--help":
|
| 24 |
+
sys.argv[1] = "help"
|
| 25 |
+
mod = "fontTools." + sys.argv[1]
|
| 26 |
+
sys.argv[1] = sys.argv[0] + " " + sys.argv[1]
|
| 27 |
+
del sys.argv[0]
|
| 28 |
+
|
| 29 |
+
import runpy
|
| 30 |
+
|
| 31 |
+
runpy.run_module(mod, run_name="__main__")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
if __name__ == "__main__":
|
| 35 |
+
sys.exit(main())
|
venv/lib/python3.10/site-packages/fontTools/afmLib.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module for reading and writing AFM (Adobe Font Metrics) files.
|
| 2 |
+
|
| 3 |
+
Note that this has been designed to read in AFM files generated by Fontographer
|
| 4 |
+
and has not been tested on many other files. In particular, it does not
|
| 5 |
+
implement the whole Adobe AFM specification [#f1]_ but, it should read most
|
| 6 |
+
"common" AFM files.
|
| 7 |
+
|
| 8 |
+
Here is an example of using `afmLib` to read, modify and write an AFM file:
|
| 9 |
+
|
| 10 |
+
>>> from fontTools.afmLib import AFM
|
| 11 |
+
>>> f = AFM("Tests/afmLib/data/TestAFM.afm")
|
| 12 |
+
>>>
|
| 13 |
+
>>> # Accessing a pair gets you the kern value
|
| 14 |
+
>>> f[("V","A")]
|
| 15 |
+
-60
|
| 16 |
+
>>>
|
| 17 |
+
>>> # Accessing a glyph name gets you metrics
|
| 18 |
+
>>> f["A"]
|
| 19 |
+
(65, 668, (8, -25, 660, 666))
|
| 20 |
+
>>> # (charnum, width, bounding box)
|
| 21 |
+
>>>
|
| 22 |
+
>>> # Accessing an attribute gets you metadata
|
| 23 |
+
>>> f.FontName
|
| 24 |
+
'TestFont-Regular'
|
| 25 |
+
>>> f.FamilyName
|
| 26 |
+
'TestFont'
|
| 27 |
+
>>> f.Weight
|
| 28 |
+
'Regular'
|
| 29 |
+
>>> f.XHeight
|
| 30 |
+
500
|
| 31 |
+
>>> f.Ascender
|
| 32 |
+
750
|
| 33 |
+
>>>
|
| 34 |
+
>>> # Attributes and items can also be set
|
| 35 |
+
>>> f[("A","V")] = -150 # Tighten kerning
|
| 36 |
+
>>> f.FontName = "TestFont Squished"
|
| 37 |
+
>>>
|
| 38 |
+
>>> # And the font written out again (remove the # in front)
|
| 39 |
+
>>> #f.write("testfont-squished.afm")
|
| 40 |
+
|
| 41 |
+
.. rubric:: Footnotes
|
| 42 |
+
|
| 43 |
+
.. [#f1] `Adobe Technote 5004 <https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5004.AFM_Spec.pdf>`_,
|
| 44 |
+
Adobe Font Metrics File Format Specification.
|
| 45 |
+
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
import re
|
| 49 |
+
|
| 50 |
+
# every single line starts with a "word"
|
| 51 |
+
identifierRE = re.compile(r"^([A-Za-z]+).*")
|
| 52 |
+
|
| 53 |
+
# regular expression to parse char lines
|
| 54 |
+
charRE = re.compile(
|
| 55 |
+
r"(-?\d+)" # charnum
|
| 56 |
+
r"\s*;\s*WX\s+" # ; WX
|
| 57 |
+
r"(-?\d+)" # width
|
| 58 |
+
r"\s*;\s*N\s+" # ; N
|
| 59 |
+
r"([.A-Za-z0-9_]+)" # charname
|
| 60 |
+
r"\s*;\s*B\s+" # ; B
|
| 61 |
+
r"(-?\d+)" # left
|
| 62 |
+
r"\s+"
|
| 63 |
+
r"(-?\d+)" # bottom
|
| 64 |
+
r"\s+"
|
| 65 |
+
r"(-?\d+)" # right
|
| 66 |
+
r"\s+"
|
| 67 |
+
r"(-?\d+)" # top
|
| 68 |
+
r"\s*;\s*" # ;
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
# regular expression to parse kerning lines
|
| 72 |
+
kernRE = re.compile(
|
| 73 |
+
r"([.A-Za-z0-9_]+)" # leftchar
|
| 74 |
+
r"\s+"
|
| 75 |
+
r"([.A-Za-z0-9_]+)" # rightchar
|
| 76 |
+
r"\s+"
|
| 77 |
+
r"(-?\d+)" # value
|
| 78 |
+
r"\s*"
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
# regular expressions to parse composite info lines of the form:
|
| 82 |
+
# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ;
|
| 83 |
+
compositeRE = re.compile(
|
| 84 |
+
r"([.A-Za-z0-9_]+)" # char name
|
| 85 |
+
r"\s+"
|
| 86 |
+
r"(\d+)" # number of parts
|
| 87 |
+
r"\s*;\s*"
|
| 88 |
+
)
|
| 89 |
+
componentRE = re.compile(
|
| 90 |
+
r"PCC\s+" # PPC
|
| 91 |
+
r"([.A-Za-z0-9_]+)" # base char name
|
| 92 |
+
r"\s+"
|
| 93 |
+
r"(-?\d+)" # x offset
|
| 94 |
+
r"\s+"
|
| 95 |
+
r"(-?\d+)" # y offset
|
| 96 |
+
r"\s*;\s*"
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
preferredAttributeOrder = [
|
| 100 |
+
"FontName",
|
| 101 |
+
"FullName",
|
| 102 |
+
"FamilyName",
|
| 103 |
+
"Weight",
|
| 104 |
+
"ItalicAngle",
|
| 105 |
+
"IsFixedPitch",
|
| 106 |
+
"FontBBox",
|
| 107 |
+
"UnderlinePosition",
|
| 108 |
+
"UnderlineThickness",
|
| 109 |
+
"Version",
|
| 110 |
+
"Notice",
|
| 111 |
+
"EncodingScheme",
|
| 112 |
+
"CapHeight",
|
| 113 |
+
"XHeight",
|
| 114 |
+
"Ascender",
|
| 115 |
+
"Descender",
|
| 116 |
+
]
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class error(Exception):
|
| 120 |
+
pass
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class AFM(object):
|
| 124 |
+
_attrs = None
|
| 125 |
+
|
| 126 |
+
_keywords = [
|
| 127 |
+
"StartFontMetrics",
|
| 128 |
+
"EndFontMetrics",
|
| 129 |
+
"StartCharMetrics",
|
| 130 |
+
"EndCharMetrics",
|
| 131 |
+
"StartKernData",
|
| 132 |
+
"StartKernPairs",
|
| 133 |
+
"EndKernPairs",
|
| 134 |
+
"EndKernData",
|
| 135 |
+
"StartComposites",
|
| 136 |
+
"EndComposites",
|
| 137 |
+
]
|
| 138 |
+
|
| 139 |
+
def __init__(self, path=None):
|
| 140 |
+
"""AFM file reader.
|
| 141 |
+
|
| 142 |
+
Instantiating an object with a path name will cause the file to be opened,
|
| 143 |
+
read, and parsed. Alternatively the path can be left unspecified, and a
|
| 144 |
+
file can be parsed later with the :meth:`read` method."""
|
| 145 |
+
self._attrs = {}
|
| 146 |
+
self._chars = {}
|
| 147 |
+
self._kerning = {}
|
| 148 |
+
self._index = {}
|
| 149 |
+
self._comments = []
|
| 150 |
+
self._composites = {}
|
| 151 |
+
if path is not None:
|
| 152 |
+
self.read(path)
|
| 153 |
+
|
| 154 |
+
def read(self, path):
|
| 155 |
+
"""Opens, reads and parses a file."""
|
| 156 |
+
lines = readlines(path)
|
| 157 |
+
for line in lines:
|
| 158 |
+
if not line.strip():
|
| 159 |
+
continue
|
| 160 |
+
m = identifierRE.match(line)
|
| 161 |
+
if m is None:
|
| 162 |
+
raise error("syntax error in AFM file: " + repr(line))
|
| 163 |
+
|
| 164 |
+
pos = m.regs[1][1]
|
| 165 |
+
word = line[:pos]
|
| 166 |
+
rest = line[pos:].strip()
|
| 167 |
+
if word in self._keywords:
|
| 168 |
+
continue
|
| 169 |
+
if word == "C":
|
| 170 |
+
self.parsechar(rest)
|
| 171 |
+
elif word == "KPX":
|
| 172 |
+
self.parsekernpair(rest)
|
| 173 |
+
elif word == "CC":
|
| 174 |
+
self.parsecomposite(rest)
|
| 175 |
+
else:
|
| 176 |
+
self.parseattr(word, rest)
|
| 177 |
+
|
| 178 |
+
def parsechar(self, rest):
|
| 179 |
+
m = charRE.match(rest)
|
| 180 |
+
if m is None:
|
| 181 |
+
raise error("syntax error in AFM file: " + repr(rest))
|
| 182 |
+
things = []
|
| 183 |
+
for fr, to in m.regs[1:]:
|
| 184 |
+
things.append(rest[fr:to])
|
| 185 |
+
charname = things[2]
|
| 186 |
+
del things[2]
|
| 187 |
+
charnum, width, l, b, r, t = (int(thing) for thing in things)
|
| 188 |
+
self._chars[charname] = charnum, width, (l, b, r, t)
|
| 189 |
+
|
| 190 |
+
def parsekernpair(self, rest):
|
| 191 |
+
m = kernRE.match(rest)
|
| 192 |
+
if m is None:
|
| 193 |
+
raise error("syntax error in AFM file: " + repr(rest))
|
| 194 |
+
things = []
|
| 195 |
+
for fr, to in m.regs[1:]:
|
| 196 |
+
things.append(rest[fr:to])
|
| 197 |
+
leftchar, rightchar, value = things
|
| 198 |
+
value = int(value)
|
| 199 |
+
self._kerning[(leftchar, rightchar)] = value
|
| 200 |
+
|
| 201 |
+
def parseattr(self, word, rest):
|
| 202 |
+
if word == "FontBBox":
|
| 203 |
+
l, b, r, t = [int(thing) for thing in rest.split()]
|
| 204 |
+
self._attrs[word] = l, b, r, t
|
| 205 |
+
elif word == "Comment":
|
| 206 |
+
self._comments.append(rest)
|
| 207 |
+
else:
|
| 208 |
+
try:
|
| 209 |
+
value = int(rest)
|
| 210 |
+
except (ValueError, OverflowError):
|
| 211 |
+
self._attrs[word] = rest
|
| 212 |
+
else:
|
| 213 |
+
self._attrs[word] = value
|
| 214 |
+
|
| 215 |
+
def parsecomposite(self, rest):
|
| 216 |
+
m = compositeRE.match(rest)
|
| 217 |
+
if m is None:
|
| 218 |
+
raise error("syntax error in AFM file: " + repr(rest))
|
| 219 |
+
charname = m.group(1)
|
| 220 |
+
ncomponents = int(m.group(2))
|
| 221 |
+
rest = rest[m.regs[0][1] :]
|
| 222 |
+
components = []
|
| 223 |
+
while True:
|
| 224 |
+
m = componentRE.match(rest)
|
| 225 |
+
if m is None:
|
| 226 |
+
raise error("syntax error in AFM file: " + repr(rest))
|
| 227 |
+
basechar = m.group(1)
|
| 228 |
+
xoffset = int(m.group(2))
|
| 229 |
+
yoffset = int(m.group(3))
|
| 230 |
+
components.append((basechar, xoffset, yoffset))
|
| 231 |
+
rest = rest[m.regs[0][1] :]
|
| 232 |
+
if not rest:
|
| 233 |
+
break
|
| 234 |
+
assert len(components) == ncomponents
|
| 235 |
+
self._composites[charname] = components
|
| 236 |
+
|
| 237 |
+
def write(self, path, sep="\r"):
|
| 238 |
+
"""Writes out an AFM font to the given path."""
|
| 239 |
+
import time
|
| 240 |
+
|
| 241 |
+
lines = [
|
| 242 |
+
"StartFontMetrics 2.0",
|
| 243 |
+
"Comment Generated by afmLib; at %s"
|
| 244 |
+
% (time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(time.time()))),
|
| 245 |
+
]
|
| 246 |
+
|
| 247 |
+
# write comments, assuming (possibly wrongly!) they should
|
| 248 |
+
# all appear at the top
|
| 249 |
+
for comment in self._comments:
|
| 250 |
+
lines.append("Comment " + comment)
|
| 251 |
+
|
| 252 |
+
# write attributes, first the ones we know about, in
|
| 253 |
+
# a preferred order
|
| 254 |
+
attrs = self._attrs
|
| 255 |
+
for attr in preferredAttributeOrder:
|
| 256 |
+
if attr in attrs:
|
| 257 |
+
value = attrs[attr]
|
| 258 |
+
if attr == "FontBBox":
|
| 259 |
+
value = "%s %s %s %s" % value
|
| 260 |
+
lines.append(attr + " " + str(value))
|
| 261 |
+
# then write the attributes we don't know about,
|
| 262 |
+
# in alphabetical order
|
| 263 |
+
items = sorted(attrs.items())
|
| 264 |
+
for attr, value in items:
|
| 265 |
+
if attr in preferredAttributeOrder:
|
| 266 |
+
continue
|
| 267 |
+
lines.append(attr + " " + str(value))
|
| 268 |
+
|
| 269 |
+
# write char metrics
|
| 270 |
+
lines.append("StartCharMetrics " + repr(len(self._chars)))
|
| 271 |
+
items = [
|
| 272 |
+
(charnum, (charname, width, box))
|
| 273 |
+
for charname, (charnum, width, box) in self._chars.items()
|
| 274 |
+
]
|
| 275 |
+
|
| 276 |
+
def myKey(a):
|
| 277 |
+
"""Custom key function to make sure unencoded chars (-1)
|
| 278 |
+
end up at the end of the list after sorting."""
|
| 279 |
+
if a[0] == -1:
|
| 280 |
+
a = (0xFFFF,) + a[1:] # 0xffff is an arbitrary large number
|
| 281 |
+
return a
|
| 282 |
+
|
| 283 |
+
items.sort(key=myKey)
|
| 284 |
+
|
| 285 |
+
for charnum, (charname, width, (l, b, r, t)) in items:
|
| 286 |
+
lines.append(
|
| 287 |
+
"C %d ; WX %d ; N %s ; B %d %d %d %d ;"
|
| 288 |
+
% (charnum, width, charname, l, b, r, t)
|
| 289 |
+
)
|
| 290 |
+
lines.append("EndCharMetrics")
|
| 291 |
+
|
| 292 |
+
# write kerning info
|
| 293 |
+
lines.append("StartKernData")
|
| 294 |
+
lines.append("StartKernPairs " + repr(len(self._kerning)))
|
| 295 |
+
items = sorted(self._kerning.items())
|
| 296 |
+
for (leftchar, rightchar), value in items:
|
| 297 |
+
lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
|
| 298 |
+
lines.append("EndKernPairs")
|
| 299 |
+
lines.append("EndKernData")
|
| 300 |
+
|
| 301 |
+
if self._composites:
|
| 302 |
+
composites = sorted(self._composites.items())
|
| 303 |
+
lines.append("StartComposites %s" % len(self._composites))
|
| 304 |
+
for charname, components in composites:
|
| 305 |
+
line = "CC %s %s ;" % (charname, len(components))
|
| 306 |
+
for basechar, xoffset, yoffset in components:
|
| 307 |
+
line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset)
|
| 308 |
+
lines.append(line)
|
| 309 |
+
lines.append("EndComposites")
|
| 310 |
+
|
| 311 |
+
lines.append("EndFontMetrics")
|
| 312 |
+
|
| 313 |
+
writelines(path, lines, sep)
|
| 314 |
+
|
| 315 |
+
def has_kernpair(self, pair):
|
| 316 |
+
"""Returns `True` if the given glyph pair (specified as a tuple) exists
|
| 317 |
+
in the kerning dictionary."""
|
| 318 |
+
return pair in self._kerning
|
| 319 |
+
|
| 320 |
+
def kernpairs(self):
|
| 321 |
+
"""Returns a list of all kern pairs in the kerning dictionary."""
|
| 322 |
+
return list(self._kerning.keys())
|
| 323 |
+
|
| 324 |
+
def has_char(self, char):
|
| 325 |
+
"""Returns `True` if the given glyph exists in the font."""
|
| 326 |
+
return char in self._chars
|
| 327 |
+
|
| 328 |
+
def chars(self):
|
| 329 |
+
"""Returns a list of all glyph names in the font."""
|
| 330 |
+
return list(self._chars.keys())
|
| 331 |
+
|
| 332 |
+
def comments(self):
|
| 333 |
+
"""Returns all comments from the file."""
|
| 334 |
+
return self._comments
|
| 335 |
+
|
| 336 |
+
def addComment(self, comment):
|
| 337 |
+
"""Adds a new comment to the file."""
|
| 338 |
+
self._comments.append(comment)
|
| 339 |
+
|
| 340 |
+
def addComposite(self, glyphName, components):
|
| 341 |
+
"""Specifies that the glyph `glyphName` is made up of the given components.
|
| 342 |
+
The components list should be of the following form::
|
| 343 |
+
|
| 344 |
+
[
|
| 345 |
+
(glyphname, xOffset, yOffset),
|
| 346 |
+
...
|
| 347 |
+
]
|
| 348 |
+
|
| 349 |
+
"""
|
| 350 |
+
self._composites[glyphName] = components
|
| 351 |
+
|
| 352 |
+
def __getattr__(self, attr):
|
| 353 |
+
if attr in self._attrs:
|
| 354 |
+
return self._attrs[attr]
|
| 355 |
+
else:
|
| 356 |
+
raise AttributeError(attr)
|
| 357 |
+
|
| 358 |
+
def __setattr__(self, attr, value):
|
| 359 |
+
# all attrs *not* starting with "_" are consider to be AFM keywords
|
| 360 |
+
if attr[:1] == "_":
|
| 361 |
+
self.__dict__[attr] = value
|
| 362 |
+
else:
|
| 363 |
+
self._attrs[attr] = value
|
| 364 |
+
|
| 365 |
+
def __delattr__(self, attr):
|
| 366 |
+
# all attrs *not* starting with "_" are consider to be AFM keywords
|
| 367 |
+
if attr[:1] == "_":
|
| 368 |
+
try:
|
| 369 |
+
del self.__dict__[attr]
|
| 370 |
+
except KeyError:
|
| 371 |
+
raise AttributeError(attr)
|
| 372 |
+
else:
|
| 373 |
+
try:
|
| 374 |
+
del self._attrs[attr]
|
| 375 |
+
except KeyError:
|
| 376 |
+
raise AttributeError(attr)
|
| 377 |
+
|
| 378 |
+
def __getitem__(self, key):
|
| 379 |
+
if isinstance(key, tuple):
|
| 380 |
+
# key is a tuple, return the kernpair
|
| 381 |
+
return self._kerning[key]
|
| 382 |
+
else:
|
| 383 |
+
# return the metrics instead
|
| 384 |
+
return self._chars[key]
|
| 385 |
+
|
| 386 |
+
def __setitem__(self, key, value):
|
| 387 |
+
if isinstance(key, tuple):
|
| 388 |
+
# key is a tuple, set kernpair
|
| 389 |
+
self._kerning[key] = value
|
| 390 |
+
else:
|
| 391 |
+
# set char metrics
|
| 392 |
+
self._chars[key] = value
|
| 393 |
+
|
| 394 |
+
def __delitem__(self, key):
|
| 395 |
+
if isinstance(key, tuple):
|
| 396 |
+
# key is a tuple, del kernpair
|
| 397 |
+
del self._kerning[key]
|
| 398 |
+
else:
|
| 399 |
+
# del char metrics
|
| 400 |
+
del self._chars[key]
|
| 401 |
+
|
| 402 |
+
def __repr__(self):
|
| 403 |
+
if hasattr(self, "FullName"):
|
| 404 |
+
return "<AFM object for %s>" % self.FullName
|
| 405 |
+
else:
|
| 406 |
+
return "<AFM object at %x>" % id(self)
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def readlines(path):
|
| 410 |
+
with open(path, "r", encoding="ascii") as f:
|
| 411 |
+
data = f.read()
|
| 412 |
+
return data.splitlines()
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def writelines(path, lines, sep="\r"):
|
| 416 |
+
with open(path, "w", encoding="ascii", newline=sep) as f:
|
| 417 |
+
f.write("\n".join(lines) + "\n")
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
if __name__ == "__main__":
|
| 421 |
+
import EasyDialogs
|
| 422 |
+
|
| 423 |
+
path = EasyDialogs.AskFileForOpen()
|
| 424 |
+
if path:
|
| 425 |
+
afm = AFM(path)
|
| 426 |
+
char = "A"
|
| 427 |
+
if afm.has_char(char):
|
| 428 |
+
print(afm[char]) # print charnum, width and boundingbox
|
| 429 |
+
pair = ("A", "V")
|
| 430 |
+
if afm.has_kernpair(pair):
|
| 431 |
+
print(afm[pair]) # print kerning value for pair
|
| 432 |
+
print(afm.Version) # various other afm entries have become attributes
|
| 433 |
+
print(afm.Weight)
|
| 434 |
+
# afm.comments() returns a list of all Comment lines found in the AFM
|
| 435 |
+
print(afm.comments())
|
| 436 |
+
# print afm.chars()
|
| 437 |
+
# print afm.kernpairs()
|
| 438 |
+
print(afm)
|
| 439 |
+
afm.write(path + ".muck")
|
venv/lib/python3.10/site-packages/fontTools/agl.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
venv/lib/python3.10/site-packages/fontTools/annotations.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from typing import TYPE_CHECKING, Iterable, Optional, TypeVar, Union
|
| 3 |
+
from collections.abc import Callable, Sequence
|
| 4 |
+
from fontTools.misc.filesystem._base import FS
|
| 5 |
+
from os import PathLike
|
| 6 |
+
from xml.etree.ElementTree import Element as ElementTreeElement
|
| 7 |
+
|
| 8 |
+
if TYPE_CHECKING:
|
| 9 |
+
from fontTools.ufoLib import UFOFormatVersion
|
| 10 |
+
from fontTools.ufoLib.glifLib import GLIFFormatVersion
|
| 11 |
+
from lxml.etree import _Element as LxmlElement
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
T = TypeVar("T") # Generic type
|
| 15 |
+
K = TypeVar("K") # Generic dict key type
|
| 16 |
+
V = TypeVar("V") # Generic dict value type
|
| 17 |
+
|
| 18 |
+
GlyphNameToFileNameFunc = Optional[Callable[[str, set[str]], str]]
|
| 19 |
+
ElementType = Union[ElementTreeElement, "LxmlElement"]
|
| 20 |
+
FormatVersion = Union[int, tuple[int, int]]
|
| 21 |
+
FormatVersions = Optional[Iterable[FormatVersion]]
|
| 22 |
+
GLIFFormatVersionInput = Optional[Union[int, tuple[int, int], "GLIFFormatVersion"]]
|
| 23 |
+
UFOFormatVersionInput = Optional[Union[int, tuple[int, int], "UFOFormatVersion"]]
|
| 24 |
+
IntFloat = Union[int, float]
|
| 25 |
+
KerningPair = tuple[str, str]
|
| 26 |
+
KerningDict = dict[KerningPair, IntFloat]
|
| 27 |
+
KerningGroups = dict[str, Sequence[str]]
|
| 28 |
+
KerningNested = dict[str, dict[str, IntFloat]]
|
| 29 |
+
PathStr = Union[str, PathLike[str]]
|
| 30 |
+
PathOrFS = Union[PathStr, FS]
|
venv/lib/python3.10/site-packages/fontTools/cffLib/CFF2ToCFF.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CFF2 to CFF converter."""
|
| 2 |
+
|
| 3 |
+
from fontTools.ttLib import TTFont, newTable
|
| 4 |
+
from fontTools.misc.cliTools import makeOutputFileName
|
| 5 |
+
from fontTools.misc.psCharStrings import T2StackUseExtractor
|
| 6 |
+
from fontTools.cffLib import (
|
| 7 |
+
TopDictIndex,
|
| 8 |
+
buildOrder,
|
| 9 |
+
buildDefaults,
|
| 10 |
+
topDictOperators,
|
| 11 |
+
privateDictOperators,
|
| 12 |
+
FDSelect,
|
| 13 |
+
)
|
| 14 |
+
from .transforms import desubroutinizeCharString
|
| 15 |
+
from .specializer import specializeProgram
|
| 16 |
+
from .width import optimizeWidths
|
| 17 |
+
from collections import defaultdict
|
| 18 |
+
import logging
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
__all__ = ["convertCFF2ToCFF", "main"]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
log = logging.getLogger("fontTools.cffLib")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _convertCFF2ToCFF(cff, otFont):
|
| 28 |
+
"""Converts this object from CFF2 format to CFF format. This conversion
|
| 29 |
+
is done 'in-place'. The conversion cannot be reversed.
|
| 30 |
+
|
| 31 |
+
The CFF2 font cannot be variable. (TODO Accept those and convert to the
|
| 32 |
+
default instance?)
|
| 33 |
+
|
| 34 |
+
This assumes a decompiled CFF2 table. (i.e. that the object has been
|
| 35 |
+
filled via :meth:`decompile` and e.g. not loaded from XML.)"""
|
| 36 |
+
|
| 37 |
+
cff.major = 1
|
| 38 |
+
|
| 39 |
+
topDictData = TopDictIndex(None)
|
| 40 |
+
for item in cff.topDictIndex:
|
| 41 |
+
# Iterate over, such that all are decompiled
|
| 42 |
+
item.cff2GetGlyphOrder = None
|
| 43 |
+
topDictData.append(item)
|
| 44 |
+
cff.topDictIndex = topDictData
|
| 45 |
+
topDict = topDictData[0]
|
| 46 |
+
|
| 47 |
+
if hasattr(topDict, "VarStore"):
|
| 48 |
+
raise ValueError("Variable CFF2 font cannot be converted to CFF format.")
|
| 49 |
+
|
| 50 |
+
opOrder = buildOrder(topDictOperators)
|
| 51 |
+
topDict.order = opOrder
|
| 52 |
+
for key in topDict.rawDict.keys():
|
| 53 |
+
if key not in opOrder:
|
| 54 |
+
del topDict.rawDict[key]
|
| 55 |
+
if hasattr(topDict, key):
|
| 56 |
+
delattr(topDict, key)
|
| 57 |
+
|
| 58 |
+
charStrings = topDict.CharStrings
|
| 59 |
+
|
| 60 |
+
fdArray = topDict.FDArray
|
| 61 |
+
if not hasattr(topDict, "FDSelect"):
|
| 62 |
+
# FDSelect is optional in CFF2, but required in CFF.
|
| 63 |
+
fdSelect = topDict.FDSelect = FDSelect()
|
| 64 |
+
fdSelect.gidArray = [0] * len(charStrings.charStrings)
|
| 65 |
+
|
| 66 |
+
defaults = buildDefaults(privateDictOperators)
|
| 67 |
+
order = buildOrder(privateDictOperators)
|
| 68 |
+
for fd in fdArray:
|
| 69 |
+
fd.setCFF2(False)
|
| 70 |
+
privateDict = fd.Private
|
| 71 |
+
privateDict.order = order
|
| 72 |
+
for key in order:
|
| 73 |
+
if key not in privateDict.rawDict and key in defaults:
|
| 74 |
+
privateDict.rawDict[key] = defaults[key]
|
| 75 |
+
for key in privateDict.rawDict.keys():
|
| 76 |
+
if key not in order:
|
| 77 |
+
del privateDict.rawDict[key]
|
| 78 |
+
if hasattr(privateDict, key):
|
| 79 |
+
delattr(privateDict, key)
|
| 80 |
+
|
| 81 |
+
# Add ending operators
|
| 82 |
+
for cs in charStrings.values():
|
| 83 |
+
cs.decompile()
|
| 84 |
+
cs.program.append("endchar")
|
| 85 |
+
for subrSets in [cff.GlobalSubrs] + [
|
| 86 |
+
getattr(fd.Private, "Subrs", []) for fd in fdArray
|
| 87 |
+
]:
|
| 88 |
+
for cs in subrSets:
|
| 89 |
+
cs.program.append("return")
|
| 90 |
+
|
| 91 |
+
# Add (optimal) width to CharStrings that need it.
|
| 92 |
+
widths = defaultdict(list)
|
| 93 |
+
metrics = otFont["hmtx"].metrics
|
| 94 |
+
for glyphName in charStrings.keys():
|
| 95 |
+
cs, fdIndex = charStrings.getItemAndSelector(glyphName)
|
| 96 |
+
if fdIndex == None:
|
| 97 |
+
fdIndex = 0
|
| 98 |
+
widths[fdIndex].append(metrics[glyphName][0])
|
| 99 |
+
for fdIndex, widthList in widths.items():
|
| 100 |
+
bestDefault, bestNominal = optimizeWidths(widthList)
|
| 101 |
+
private = fdArray[fdIndex].Private
|
| 102 |
+
private.defaultWidthX = bestDefault
|
| 103 |
+
private.nominalWidthX = bestNominal
|
| 104 |
+
for glyphName in charStrings.keys():
|
| 105 |
+
cs, fdIndex = charStrings.getItemAndSelector(glyphName)
|
| 106 |
+
if fdIndex == None:
|
| 107 |
+
fdIndex = 0
|
| 108 |
+
private = fdArray[fdIndex].Private
|
| 109 |
+
width = metrics[glyphName][0]
|
| 110 |
+
if width != private.defaultWidthX:
|
| 111 |
+
cs.program.insert(0, width - private.nominalWidthX)
|
| 112 |
+
|
| 113 |
+
# Handle stack use since stack-depth is lower in CFF than in CFF2.
|
| 114 |
+
for glyphName in charStrings.keys():
|
| 115 |
+
cs, fdIndex = charStrings.getItemAndSelector(glyphName)
|
| 116 |
+
if fdIndex is None:
|
| 117 |
+
fdIndex = 0
|
| 118 |
+
private = fdArray[fdIndex].Private
|
| 119 |
+
extractor = T2StackUseExtractor(
|
| 120 |
+
getattr(private, "Subrs", []), cff.GlobalSubrs, private=private
|
| 121 |
+
)
|
| 122 |
+
stackUse = extractor.execute(cs)
|
| 123 |
+
if stackUse > 48: # CFF stack depth is 48
|
| 124 |
+
desubroutinizeCharString(cs)
|
| 125 |
+
cs.program = specializeProgram(cs.program)
|
| 126 |
+
|
| 127 |
+
# Unused subroutines are still in CFF2 (ie. lacking 'return' operator)
|
| 128 |
+
# because they were not decompiled when we added the 'return'.
|
| 129 |
+
# Moreover, some used subroutines may have become unused after the
|
| 130 |
+
# stack-use fixup. So we remove all unused subroutines now.
|
| 131 |
+
cff.remove_unused_subroutines()
|
| 132 |
+
|
| 133 |
+
mapping = {
|
| 134 |
+
name: ("cid" + str(n).zfill(5) if n else ".notdef")
|
| 135 |
+
for n, name in enumerate(topDict.charset)
|
| 136 |
+
}
|
| 137 |
+
topDict.charset = [
|
| 138 |
+
"cid" + str(n).zfill(5) if n else ".notdef" for n in range(len(topDict.charset))
|
| 139 |
+
]
|
| 140 |
+
charStrings.charStrings = {
|
| 141 |
+
mapping[name]: v for name, v in charStrings.charStrings.items()
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
topDict.ROS = ("Adobe", "Identity", 0)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def convertCFF2ToCFF(font, *, updatePostTable=True):
|
| 148 |
+
if "CFF2" not in font:
|
| 149 |
+
raise ValueError("Input font does not contain a CFF2 table.")
|
| 150 |
+
cff = font["CFF2"].cff
|
| 151 |
+
_convertCFF2ToCFF(cff, font)
|
| 152 |
+
del font["CFF2"]
|
| 153 |
+
table = font["CFF "] = newTable("CFF ")
|
| 154 |
+
table.cff = cff
|
| 155 |
+
|
| 156 |
+
if updatePostTable and "post" in font:
|
| 157 |
+
# Only version supported for fonts with CFF table is 0x00030000 not 0x20000
|
| 158 |
+
post = font["post"]
|
| 159 |
+
if post.formatType == 2.0:
|
| 160 |
+
post.formatType = 3.0
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def main(args=None):
|
| 164 |
+
"""Convert CFF2 OTF font to CFF OTF font"""
|
| 165 |
+
if args is None:
|
| 166 |
+
import sys
|
| 167 |
+
|
| 168 |
+
args = sys.argv[1:]
|
| 169 |
+
|
| 170 |
+
import argparse
|
| 171 |
+
|
| 172 |
+
parser = argparse.ArgumentParser(
|
| 173 |
+
"fonttools cffLib.CFF2ToCFF",
|
| 174 |
+
description="Convert a non-variable CFF2 font to CFF.",
|
| 175 |
+
)
|
| 176 |
+
parser.add_argument(
|
| 177 |
+
"input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
|
| 178 |
+
)
|
| 179 |
+
parser.add_argument(
|
| 180 |
+
"-o",
|
| 181 |
+
"--output",
|
| 182 |
+
metavar="OUTPUT.ttf",
|
| 183 |
+
default=None,
|
| 184 |
+
help="Output instance OTF file (default: INPUT-CFF2.ttf).",
|
| 185 |
+
)
|
| 186 |
+
parser.add_argument(
|
| 187 |
+
"--no-recalc-timestamp",
|
| 188 |
+
dest="recalc_timestamp",
|
| 189 |
+
action="store_false",
|
| 190 |
+
help="Don't set the output font's timestamp to the current time.",
|
| 191 |
+
)
|
| 192 |
+
loggingGroup = parser.add_mutually_exclusive_group(required=False)
|
| 193 |
+
loggingGroup.add_argument(
|
| 194 |
+
"-v", "--verbose", action="store_true", help="Run more verbosely."
|
| 195 |
+
)
|
| 196 |
+
loggingGroup.add_argument(
|
| 197 |
+
"-q", "--quiet", action="store_true", help="Turn verbosity off."
|
| 198 |
+
)
|
| 199 |
+
options = parser.parse_args(args)
|
| 200 |
+
|
| 201 |
+
from fontTools import configLogger
|
| 202 |
+
|
| 203 |
+
configLogger(
|
| 204 |
+
level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
import os
|
| 208 |
+
|
| 209 |
+
infile = options.input
|
| 210 |
+
if not os.path.isfile(infile):
|
| 211 |
+
parser.error("No such file '{}'".format(infile))
|
| 212 |
+
|
| 213 |
+
outfile = (
|
| 214 |
+
makeOutputFileName(infile, overWrite=True, suffix="-CFF")
|
| 215 |
+
if not options.output
|
| 216 |
+
else options.output
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
|
| 220 |
+
|
| 221 |
+
convertCFF2ToCFF(font)
|
| 222 |
+
|
| 223 |
+
log.info(
|
| 224 |
+
"Saving %s",
|
| 225 |
+
outfile,
|
| 226 |
+
)
|
| 227 |
+
font.save(outfile)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
if __name__ == "__main__":
|
| 231 |
+
import sys
|
| 232 |
+
|
| 233 |
+
sys.exit(main(sys.argv[1:]))
|
venv/lib/python3.10/site-packages/fontTools/cffLib/CFFToCFF2.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CFF to CFF2 converter."""
|
| 2 |
+
|
| 3 |
+
from fontTools.ttLib import TTFont, newTable
|
| 4 |
+
from fontTools.misc.cliTools import makeOutputFileName
|
| 5 |
+
from fontTools.misc.psCharStrings import T2WidthExtractor
|
| 6 |
+
from fontTools.cffLib import (
|
| 7 |
+
TopDictIndex,
|
| 8 |
+
FDArrayIndex,
|
| 9 |
+
FontDict,
|
| 10 |
+
buildOrder,
|
| 11 |
+
topDictOperators,
|
| 12 |
+
privateDictOperators,
|
| 13 |
+
topDictOperators2,
|
| 14 |
+
privateDictOperators2,
|
| 15 |
+
)
|
| 16 |
+
from io import BytesIO
|
| 17 |
+
import logging
|
| 18 |
+
|
| 19 |
+
__all__ = ["convertCFFToCFF2", "main"]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
log = logging.getLogger("fontTools.cffLib")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class _NominalWidthUsedError(Exception):
|
| 26 |
+
def __add__(self, other):
|
| 27 |
+
raise self
|
| 28 |
+
|
| 29 |
+
def __radd__(self, other):
|
| 30 |
+
raise self
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _convertCFFToCFF2(cff, otFont):
|
| 34 |
+
"""Converts this object from CFF format to CFF2 format. This conversion
|
| 35 |
+
is done 'in-place'. The conversion cannot be reversed.
|
| 36 |
+
|
| 37 |
+
This assumes a decompiled CFF table. (i.e. that the object has been
|
| 38 |
+
filled via :meth:`decompile` and e.g. not loaded from XML.)"""
|
| 39 |
+
|
| 40 |
+
# Clean up T2CharStrings
|
| 41 |
+
|
| 42 |
+
topDict = cff.topDictIndex[0]
|
| 43 |
+
fdArray = topDict.FDArray if hasattr(topDict, "FDArray") else None
|
| 44 |
+
charStrings = topDict.CharStrings
|
| 45 |
+
globalSubrs = cff.GlobalSubrs
|
| 46 |
+
localSubrs = (
|
| 47 |
+
[getattr(fd.Private, "Subrs", []) for fd in fdArray]
|
| 48 |
+
if fdArray
|
| 49 |
+
else (
|
| 50 |
+
[topDict.Private.Subrs]
|
| 51 |
+
if hasattr(topDict, "Private") and hasattr(topDict.Private, "Subrs")
|
| 52 |
+
else []
|
| 53 |
+
)
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
for glyphName in charStrings.keys():
|
| 57 |
+
cs, fdIndex = charStrings.getItemAndSelector(glyphName)
|
| 58 |
+
cs.decompile()
|
| 59 |
+
|
| 60 |
+
# Clean up subroutines first
|
| 61 |
+
for subrs in [globalSubrs] + localSubrs:
|
| 62 |
+
for subr in subrs:
|
| 63 |
+
program = subr.program
|
| 64 |
+
i = j = len(program)
|
| 65 |
+
try:
|
| 66 |
+
i = program.index("return")
|
| 67 |
+
except ValueError:
|
| 68 |
+
pass
|
| 69 |
+
try:
|
| 70 |
+
j = program.index("endchar")
|
| 71 |
+
except ValueError:
|
| 72 |
+
pass
|
| 73 |
+
program[min(i, j) :] = []
|
| 74 |
+
|
| 75 |
+
# Clean up glyph charstrings
|
| 76 |
+
removeUnusedSubrs = False
|
| 77 |
+
nominalWidthXError = _NominalWidthUsedError()
|
| 78 |
+
for glyphName in charStrings.keys():
|
| 79 |
+
cs, fdIndex = charStrings.getItemAndSelector(glyphName)
|
| 80 |
+
program = cs.program
|
| 81 |
+
|
| 82 |
+
thisLocalSubrs = (
|
| 83 |
+
localSubrs[fdIndex]
|
| 84 |
+
if fdIndex is not None
|
| 85 |
+
else (
|
| 86 |
+
getattr(topDict.Private, "Subrs", [])
|
| 87 |
+
if hasattr(topDict, "Private")
|
| 88 |
+
else []
|
| 89 |
+
)
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# Intentionally use custom type for nominalWidthX, such that any
|
| 93 |
+
# CharString that has an explicit width encoded will throw back to us.
|
| 94 |
+
extractor = T2WidthExtractor(
|
| 95 |
+
thisLocalSubrs,
|
| 96 |
+
globalSubrs,
|
| 97 |
+
nominalWidthXError,
|
| 98 |
+
0,
|
| 99 |
+
)
|
| 100 |
+
try:
|
| 101 |
+
extractor.execute(cs)
|
| 102 |
+
except _NominalWidthUsedError:
|
| 103 |
+
# Program has explicit width. We want to drop it, but can't
|
| 104 |
+
# just pop the first number since it may be a subroutine call.
|
| 105 |
+
# Instead, when seeing that, we embed the subroutine and recurse.
|
| 106 |
+
# If this ever happened, we later prune unused subroutines.
|
| 107 |
+
while len(program) >= 2 and program[1] in ["callsubr", "callgsubr"]:
|
| 108 |
+
removeUnusedSubrs = True
|
| 109 |
+
subrNumber = program.pop(0)
|
| 110 |
+
assert isinstance(subrNumber, int), subrNumber
|
| 111 |
+
op = program.pop(0)
|
| 112 |
+
bias = extractor.localBias if op == "callsubr" else extractor.globalBias
|
| 113 |
+
subrNumber += bias
|
| 114 |
+
subrSet = thisLocalSubrs if op == "callsubr" else globalSubrs
|
| 115 |
+
subrProgram = subrSet[subrNumber].program
|
| 116 |
+
program[:0] = subrProgram
|
| 117 |
+
# Now pop the actual width
|
| 118 |
+
assert len(program) >= 1, program
|
| 119 |
+
program.pop(0)
|
| 120 |
+
|
| 121 |
+
if program and program[-1] == "endchar":
|
| 122 |
+
program.pop()
|
| 123 |
+
|
| 124 |
+
if removeUnusedSubrs:
|
| 125 |
+
cff.remove_unused_subroutines()
|
| 126 |
+
|
| 127 |
+
# Upconvert TopDict
|
| 128 |
+
|
| 129 |
+
cff.major = 2
|
| 130 |
+
cff2GetGlyphOrder = cff.otFont.getGlyphOrder
|
| 131 |
+
topDictData = TopDictIndex(None, cff2GetGlyphOrder)
|
| 132 |
+
for item in cff.topDictIndex:
|
| 133 |
+
# Iterate over, such that all are decompiled
|
| 134 |
+
topDictData.append(item)
|
| 135 |
+
cff.topDictIndex = topDictData
|
| 136 |
+
topDict = topDictData[0]
|
| 137 |
+
if hasattr(topDict, "Private"):
|
| 138 |
+
privateDict = topDict.Private
|
| 139 |
+
else:
|
| 140 |
+
privateDict = None
|
| 141 |
+
opOrder = buildOrder(topDictOperators2)
|
| 142 |
+
topDict.order = opOrder
|
| 143 |
+
topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
|
| 144 |
+
|
| 145 |
+
if not hasattr(topDict, "FDArray"):
|
| 146 |
+
fdArray = topDict.FDArray = FDArrayIndex()
|
| 147 |
+
fdArray.strings = None
|
| 148 |
+
fdArray.GlobalSubrs = topDict.GlobalSubrs
|
| 149 |
+
topDict.GlobalSubrs.fdArray = fdArray
|
| 150 |
+
charStrings = topDict.CharStrings
|
| 151 |
+
if charStrings.charStringsAreIndexed:
|
| 152 |
+
charStrings.charStringsIndex.fdArray = fdArray
|
| 153 |
+
else:
|
| 154 |
+
charStrings.fdArray = fdArray
|
| 155 |
+
fontDict = FontDict()
|
| 156 |
+
fontDict.setCFF2(True)
|
| 157 |
+
fdArray.append(fontDict)
|
| 158 |
+
fontDict.Private = privateDict
|
| 159 |
+
privateOpOrder = buildOrder(privateDictOperators2)
|
| 160 |
+
if privateDict is not None:
|
| 161 |
+
for entry in privateDictOperators:
|
| 162 |
+
key = entry[1]
|
| 163 |
+
if key not in privateOpOrder:
|
| 164 |
+
if key in privateDict.rawDict:
|
| 165 |
+
# print "Removing private dict", key
|
| 166 |
+
del privateDict.rawDict[key]
|
| 167 |
+
if hasattr(privateDict, key):
|
| 168 |
+
delattr(privateDict, key)
|
| 169 |
+
# print "Removing privateDict attr", key
|
| 170 |
+
else:
|
| 171 |
+
# clean up the PrivateDicts in the fdArray
|
| 172 |
+
fdArray = topDict.FDArray
|
| 173 |
+
privateOpOrder = buildOrder(privateDictOperators2)
|
| 174 |
+
for fontDict in fdArray:
|
| 175 |
+
fontDict.setCFF2(True)
|
| 176 |
+
for key in list(fontDict.rawDict.keys()):
|
| 177 |
+
if key not in fontDict.order:
|
| 178 |
+
del fontDict.rawDict[key]
|
| 179 |
+
if hasattr(fontDict, key):
|
| 180 |
+
delattr(fontDict, key)
|
| 181 |
+
|
| 182 |
+
privateDict = fontDict.Private
|
| 183 |
+
for entry in privateDictOperators:
|
| 184 |
+
key = entry[1]
|
| 185 |
+
if key not in privateOpOrder:
|
| 186 |
+
if key in list(privateDict.rawDict.keys()):
|
| 187 |
+
# print "Removing private dict", key
|
| 188 |
+
del privateDict.rawDict[key]
|
| 189 |
+
if hasattr(privateDict, key):
|
| 190 |
+
delattr(privateDict, key)
|
| 191 |
+
# print "Removing privateDict attr", key
|
| 192 |
+
|
| 193 |
+
# Now delete up the deprecated topDict operators from CFF 1.0
|
| 194 |
+
for entry in topDictOperators:
|
| 195 |
+
key = entry[1]
|
| 196 |
+
# We seem to need to keep the charset operator for now,
|
| 197 |
+
# or we fail to compile with some fonts, like AdditionFont.otf.
|
| 198 |
+
# I don't know which kind of CFF font those are. But keeping
|
| 199 |
+
# charset seems to work. It will be removed when we save and
|
| 200 |
+
# read the font again.
|
| 201 |
+
#
|
| 202 |
+
# AdditionFont.otf has <Encoding name="StandardEncoding"/>.
|
| 203 |
+
if key == "charset":
|
| 204 |
+
continue
|
| 205 |
+
if key not in opOrder:
|
| 206 |
+
if key in topDict.rawDict:
|
| 207 |
+
del topDict.rawDict[key]
|
| 208 |
+
if hasattr(topDict, key):
|
| 209 |
+
delattr(topDict, key)
|
| 210 |
+
|
| 211 |
+
# TODO(behdad): What does the following comment even mean? Both CFF and CFF2
|
| 212 |
+
# use the same T2Charstring class. I *think* what it means is that the CharStrings
|
| 213 |
+
# were loaded for CFF1, and we need to reload them for CFF2 to set varstore, etc
|
| 214 |
+
# on them. At least that's what I understand. It's probably safe to remove this
|
| 215 |
+
# and just set vstore where needed.
|
| 216 |
+
#
|
| 217 |
+
# See comment above about charset as well.
|
| 218 |
+
|
| 219 |
+
# At this point, the Subrs and Charstrings are all still T2Charstring class
|
| 220 |
+
# easiest to fix this by compiling, then decompiling again
|
| 221 |
+
file = BytesIO()
|
| 222 |
+
cff.compile(file, otFont, isCFF2=True)
|
| 223 |
+
file.seek(0)
|
| 224 |
+
cff.decompile(file, otFont, isCFF2=True)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def convertCFFToCFF2(font):
|
| 228 |
+
cff = font["CFF "].cff
|
| 229 |
+
del font["CFF "]
|
| 230 |
+
_convertCFFToCFF2(cff, font)
|
| 231 |
+
table = font["CFF2"] = newTable("CFF2")
|
| 232 |
+
table.cff = cff
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def main(args=None):
|
| 236 |
+
"""Convert CFF OTF font to CFF2 OTF font"""
|
| 237 |
+
if args is None:
|
| 238 |
+
import sys
|
| 239 |
+
|
| 240 |
+
args = sys.argv[1:]
|
| 241 |
+
|
| 242 |
+
import argparse
|
| 243 |
+
|
| 244 |
+
parser = argparse.ArgumentParser(
|
| 245 |
+
"fonttools cffLib.CFFToCFF2",
|
| 246 |
+
description="Upgrade a CFF font to CFF2.",
|
| 247 |
+
)
|
| 248 |
+
parser.add_argument(
|
| 249 |
+
"input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
|
| 250 |
+
)
|
| 251 |
+
parser.add_argument(
|
| 252 |
+
"-o",
|
| 253 |
+
"--output",
|
| 254 |
+
metavar="OUTPUT.ttf",
|
| 255 |
+
default=None,
|
| 256 |
+
help="Output instance OTF file (default: INPUT-CFF2.ttf).",
|
| 257 |
+
)
|
| 258 |
+
parser.add_argument(
|
| 259 |
+
"--no-recalc-timestamp",
|
| 260 |
+
dest="recalc_timestamp",
|
| 261 |
+
action="store_false",
|
| 262 |
+
help="Don't set the output font's timestamp to the current time.",
|
| 263 |
+
)
|
| 264 |
+
loggingGroup = parser.add_mutually_exclusive_group(required=False)
|
| 265 |
+
loggingGroup.add_argument(
|
| 266 |
+
"-v", "--verbose", action="store_true", help="Run more verbosely."
|
| 267 |
+
)
|
| 268 |
+
loggingGroup.add_argument(
|
| 269 |
+
"-q", "--quiet", action="store_true", help="Turn verbosity off."
|
| 270 |
+
)
|
| 271 |
+
options = parser.parse_args(args)
|
| 272 |
+
|
| 273 |
+
from fontTools import configLogger
|
| 274 |
+
|
| 275 |
+
configLogger(
|
| 276 |
+
level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
import os
|
| 280 |
+
|
| 281 |
+
infile = options.input
|
| 282 |
+
if not os.path.isfile(infile):
|
| 283 |
+
parser.error("No such file '{}'".format(infile))
|
| 284 |
+
|
| 285 |
+
outfile = (
|
| 286 |
+
makeOutputFileName(infile, overWrite=True, suffix="-CFF2")
|
| 287 |
+
if not options.output
|
| 288 |
+
else options.output
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
|
| 292 |
+
|
| 293 |
+
convertCFFToCFF2(font)
|
| 294 |
+
|
| 295 |
+
log.info(
|
| 296 |
+
"Saving %s",
|
| 297 |
+
outfile,
|
| 298 |
+
)
|
| 299 |
+
font.save(outfile)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
if __name__ == "__main__":
|
| 303 |
+
import sys
|
| 304 |
+
|
| 305 |
+
sys.exit(main(sys.argv[1:]))
|
venv/lib/python3.10/site-packages/fontTools/cffLib/__init__.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|