Merge with main

This commit is contained in:
Francisco Manríquez Novoa 2026-02-20 22:48:07 -03:00
commit 7df4abd334
69 changed files with 745 additions and 557 deletions

View file

@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v6
- name: Install dependencies
run: apt-get update && apt-get install -y build-essential python3-dev libcairo2-dev libpango1.0-dev
run: sudo apt-get update && sudo apt-get install -y build-essential python3-dev libcairo2-dev libpango1.0-dev
- name: Set up Python 3.13
uses: actions/setup-python@v6

View file

@ -4,10 +4,10 @@ authors:
-
name: "The Manim Community Developers"
cff-version: "1.2.0"
date-released: 2026-01-17
date-released: 2026-02-20
license: MIT
message: "We acknowledge the importance of good software to support research, and we note that research becomes more valuable when it is communicated effectively. To demonstrate the value of Manim, we ask that you cite Manim in your work."
title: Manim Mathematical Animation Framework
url: "https://www.manim.community/"
version: "v0.19.2"
version: "v0.20.0"
...

View file

@ -8,6 +8,7 @@ This page contains a list of changes made between releases.
:maxdepth: 1
changelog/experimental
changelog/0.20.0-changelog
changelog/0.19.2-changelog
changelog/0.19.1-changelog
changelog/0.19.0-changelog

View file

@ -0,0 +1,86 @@
---
short-title: v0.20.0
description: Changelog for v0.20.0
---
# v0.20.0
Date
: February 20, 2026
## What's Changed
### Breaking Changes 🚨
* Fix `ImageMobject` 3D rotation/flipping and remove resampling algorithms `lanczos` (`antialias`), `box` and `hamming` by {user}`chopan050` in {pr}`4266`
* Fix `YELLOW_C` and add `PURE_CYAN`, `PURE_MAGENTA` and `PURE_YELLOW` by {user}`chopan050` in {pr}`4562`
### Highlights 🌟
* Rewrite MathTex to make it more robust regarding splitting by {user}`henrikmidtiby` in {pr}`4515`
The MathTex implementation has been updated to make it more robust and fix a number of issues.
A beneficial side effect is that named groups in svg files can now be accessed through SVGMobject.
* Add new Animation Builder `Mobject.always` by {user}`JasonGrace2282` in {pr}`4594`
This new feature is a convenience wrapper around `add_updater` that allows adding
updaters to a mobject in an intuitive and easy-to-read way. Example usage in a scene:
```python
d = Dot()
s = Square()
d.always.next_to(s, UP)
self.add(s, d)
self.play(s.animate.to_edge(LEFT))
```
### New Features ✨
* Add a `seed` config option + `--seed` CLI option for reproducible randomness in rendered scenes by {user}`arnaud-ma` in {pr}`4532`
### Enhancements 🚀
* Enable `strict=True` for `zip()` where safe by {user}`Oll-iver` in {pr}`4547`
### Bug Fixes 🐛
* using `color` instead of `fill_color` with MathTeX for node labels by {user}`Schefflera-Arboricola` in {pr}`4501`
* fix: infinite recursion caused by accessing color of a highlighted Ta… by {user}`BHearron` in {pr}`4435`
* Prevent potential `UnboundLocalError` in `PolarPlane` by {user}`RinZ27` in {pr}`4557`
* Fixed division by 0 in `turn_animation_into_updater` by {user}`SoldierSacha` in {pr}`4567`
* Fix TOCTOU Race Conditions when creating directories by {user}`SoldierSacha` in {pr}`4587`
* Resolve more race conditions potentially happening during directory creation by {user}`SoldierSacha` in {pr}`4589`
* Fix `c2p`/`coords_to_point` method call with single flat list or 1D array input by {user}`danielalanbates` in {pr}`4596`
### Documentation 📚
* Enable rendered documentation of `RandomColorGenerator` by {user}`arnaud-ma` in {pr}`4533`
* Remove pin to Python 3.13 in installation docs by {user}`chopan050` in {pr}`4534`
* Fix broken aquabeam OpenGL link using Wayback Machine by {user}`behackl` in {pr}`4545`
* Add type annotations and docstrings in `opengl_renderer.py` by {user}`arnaud-ma` in {pr}`4537`
* docs: improve `TransformFromCopy` docstring by {user}`GoThrones` in {pr}`4597`
### Infrastructure & Build 🔨
* Install missing dependencies in release pipeline by {user}`behackl` in {pr}`4531`
### Code Quality & Refactoring 🧹
* Rework and consolidate release changelog script, add previously skipped changelog entries by {user}`behackl` in {pr}`4568`
* Remove `__future__.annotations` from required imports by {user}`JasonGrace2282` in {pr}`4571`
* Cleaned up `mypy.ini` by {user}`henrikmidtiby` in {pr}`4584`
* Add `py.typed` to declare manim as having type hints by {user}`Timmmm` in {pr}`4553`
* Fix assertion in `ImageMobjectFromCamera.interpolate_color()` by {user}`chopan050` in {pr}`4593`
* Reduce dependency on scipy - replace `scipy.special.comb` with `math.comb` by {user}`fmuenkel` in {pr}`4598`
### Type Hints 📝
* Add type annotations to `rotation.py` by {user}`fmuenkel` in {pr}`4535`
* Add type annotations to `opengl_compatibility.py` by {user}`fmuenkel` in {pr}`4585`
* Add type annotations to `image_mobject.py` by {user}`henrikmidtiby` in {pr}`4458`
* Add type annotations to `opengl_image_mobject.py` by {user}`fmuenkel` in {pr}`4536`
* Add type annotations to `point_cloud_mobject.py` by {user}`fmuenkel` in {pr}`4586`
## New Contributors
* {user}`arnaud-ma` made their first contribution in {pr}`4533`
* {user}`Schefflera-Arboricola` made their first contribution in {pr}`4501`
* {user}`BHearron` made their first contribution in {pr}`4435`
* {user}`RinZ27` made their first contribution in {pr}`4557`
* {user}`SoldierSacha` made their first contribution in {pr}`4567`
* {user}`Oll-iver` made their first contribution in {pr}`4547`
* {user}`GoThrones` made their first contribution in {pr}`4597`
* {user}`danielalanbates` made their first contribution in {pr}`4596`
**Full Changelog**: [Compare view](https://github.com/ManimCommunity/manim/compare/v0.19.2...v0.20.0)

View file

@ -424,7 +424,7 @@ may be expected. To color only ``x`` yellow, we have to do the following:
class CorrectLaTeXSubstringColoring(Scene):
def construct(self):
equation = MathTex(
r"e^x = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
r"e^{x} = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
substrings_to_isolate="x"
)
equation.set_color_by_tex("x", YELLOW)
@ -434,6 +434,8 @@ By setting ``substrings_to_isolate`` to ``x``, we split up the
:class:`~.MathTex` into substrings automatically and isolate the ``x`` components
into individual substrings. Only then can :meth:`~.set_color_by_tex` be used
to achieve the desired result.
If one of the ``substrings_to_isolate`` is in a sub or superscript, it needs
to be enclosed by curly brackets.
Note that Manim also supports a custom syntax that allows splitting
a TeX string into substrings easily: simply enclose parts of your formula

View file

@ -182,7 +182,7 @@ class AnimationGroup(Animation):
sub_alphas[(sub_alphas > 1) | with_zero_run_time] = 1
for anim_to_update, sub_alpha in zip(
to_update["anim"], sub_alphas, strict=False
to_update["anim"], sub_alphas, strict=True
):
anim_to_update.interpolate(sub_alpha)

View file

@ -67,7 +67,7 @@ from ..mobject.opengl.opengl_vectorized_mobject import (
)
from ..typing import Point3D, Point3DLike, Vector3DLike
from ..utils.bezier import interpolate, inverse_interpolate
from ..utils.color import GREY, YELLOW, ParsableManimColor
from ..utils.color import GREY, PURE_YELLOW, ParsableManimColor
from ..utils.rate_functions import RateFunction, smooth, there_and_back, wiggle
from ..utils.space_ops import normalize
@ -92,7 +92,7 @@ class FocusOn(Transform):
class UsingFocusOn(Scene):
def construct(self):
dot = Dot(color=YELLOW).shift(DOWN)
dot = Dot(color=PURE_YELLOW).shift(DOWN)
self.add(Tex("Focusing on the dot below:"), dot)
self.play(FocusOn(dot))
self.wait()
@ -156,7 +156,7 @@ class Indicate(Transform):
self,
mobject: Mobject,
scale_factor: float = 1.2,
color: ParsableManimColor = YELLOW,
color: ParsableManimColor = PURE_YELLOW,
rate_func: RateFunction = there_and_back,
**kwargs: Any,
):
@ -201,7 +201,7 @@ class Flash(AnimationGroup):
class UsingFlash(Scene):
def construct(self):
dot = Dot(color=YELLOW).shift(DOWN)
dot = Dot(color=PURE_YELLOW).shift(DOWN)
self.add(Tex("Flash the dot below:"), dot)
self.play(Flash(dot))
self.wait()
@ -229,7 +229,7 @@ class Flash(AnimationGroup):
num_lines: int = 12,
flash_radius: float = 0.1,
line_stroke_width: int = 3,
color: ParsableManimColor = YELLOW,
color: ParsableManimColor = PURE_YELLOW,
time_width: float = 1,
run_time: float = 1.0,
**kwargs: Any,
@ -352,7 +352,7 @@ class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
for stroke_width, time_width in zip(
np.linspace(0, max_stroke_width, self.n_segments),
np.linspace(max_time_width, 0, self.n_segments),
strict=False,
strict=True,
)
),
)
@ -625,7 +625,7 @@ class Circumscribe(Succession):
fade_out: bool = False,
time_width: float = 0.3,
buff: float = SMALL_BUFF,
color: ParsableManimColor = YELLOW,
color: ParsableManimColor = PURE_YELLOW,
run_time: float = 1,
stroke_width: float = DEFAULT_STROKE_WIDTH,
**kwargs: Any,

View file

@ -224,7 +224,7 @@ class Transform(Animation):
self.starting_mobject,
self.target_copy,
]
return zip(*(mob.get_family() for mob in mobs))
return zip(*(mob.get_family() for mob in mobs), strict=True)
def interpolate_submobject(
self,
@ -292,7 +292,7 @@ class ReplacementTransform(Transform):
class TransformFromCopy(Transform):
"""Performs a reversed Transform"""
"""Preserves a copy of the original VMobject and transforms only it's copy to the target VMobject"""
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs) -> None:
super().__init__(target_mobject, mobject, **kwargs)
@ -730,7 +730,7 @@ class CyclicReplace(Transform):
def create_target(self) -> Group:
target = self.group.copy()
cycled_targets = [target[-1], *target[:-1]]
for m1, m2 in zip(cycled_targets, self.group, strict=False):
for m1, m2 in zip(cycled_targets, self.group, strict=True):
m1.move_to(m2)
return target
@ -915,5 +915,5 @@ class FadeTransformPieces(FadeTransform):
"""Replaces the source submobjects by the target submobjects and sets
the opacity to 0.
"""
for sm0, sm1 in zip(source.get_family(), target.get_family(), strict=False):
for sm0, sm1 in zip(source.get_family(), target.get_family(), strict=True):
super().ghost_to(sm0, sm1)

View file

@ -308,7 +308,7 @@ Are you sure you want to continue? (y/n)""",
if proceed:
if not directory_path.is_dir():
console.print(f"Creating folder: {directory}.", style="red bold")
directory_path.mkdir(parents=True)
directory_path.mkdir(parents=True, exist_ok=True)
ctx.invoke(write)
from_path = Path.cwd() / "manim.cfg"

View file

@ -110,14 +110,10 @@ ULTRAHEAVY = "ULTRAHEAVY"
RESAMPLING_ALGORITHMS = {
"nearest": Resampling.NEAREST,
"none": Resampling.NEAREST,
"lanczos": Resampling.LANCZOS,
"antialias": Resampling.LANCZOS,
"bilinear": Resampling.BILINEAR,
"linear": Resampling.BILINEAR,
"bicubic": Resampling.BICUBIC,
"cubic": Resampling.BICUBIC,
"box": Resampling.BOX,
"hamming": Resampling.HAMMING,
}
# Geometry: directions

View file

@ -1231,7 +1231,7 @@ class ArcPolygon(VMobject):
arcs = [
ArcBetweenPoints(*pair, **conf)
for (pair, conf) in zip(point_pairs, all_arc_configs, strict=False)
for (pair, conf) in zip(point_pairs, all_arc_configs, strict=True)
]
super().__init__(**kwargs)

View file

@ -156,7 +156,7 @@ class Polygram(VMobject):
# times, then .get_vertex_groups() splits it into N vertex groups.
group = []
for start, end in zip(
self.get_start_anchors(), self.get_end_anchors(), strict=False
self.get_start_anchors(), self.get_end_anchors(), strict=True
):
group.append(start)
@ -244,7 +244,7 @@ class Polygram(VMobject):
radius_list = radius * ceil(len(vertex_group) / len(radius))
for current_radius, (v1, v2, v3) in zip(
radius_list, adjacent_n_tuples(vertex_group, 3), strict=False
radius_list, adjacent_n_tuples(vertex_group, 3), strict=True
):
vect1 = v2 - v1
vect2 = v3 - v2
@ -556,7 +556,7 @@ class Star(Polygon):
)
vertices: list[npt.NDArray] = []
for pair in zip(outer_vertices, inner_vertices, strict=False):
for pair in zip(outer_vertices, inner_vertices, strict=True):
vertices.extend(pair)
super().__init__(*vertices, **kwargs)

View file

@ -19,7 +19,7 @@ from manim.mobject.geometry.line import Line
from manim.mobject.geometry.polygram import RoundedRectangle
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
from manim.utils.color import BLACK, RED, YELLOW, ParsableManimColor
from manim.utils.color import BLACK, PURE_YELLOW, RED, ParsableManimColor
class SurroundingRectangle(RoundedRectangle):
@ -49,7 +49,7 @@ class SurroundingRectangle(RoundedRectangle):
def __init__(
self,
*mobjects: Mobject,
color: ParsableManimColor = YELLOW,
color: ParsableManimColor = PURE_YELLOW,
buff: float | tuple[float, float] = SMALL_BUFF,
corner_radius: float = 0.0,
**kwargs: Any,

View file

@ -47,8 +47,8 @@ from manim.utils.color import (
BLUE,
BLUE_D,
GREEN,
PURE_YELLOW,
WHITE,
YELLOW,
ManimColor,
ParsableManimColor,
color_gradient,
@ -452,7 +452,7 @@ class CoordinateSystem:
zip(
tick_range,
axis.scaling.get_custom_labels(tick_range),
strict=False,
strict=True,
)
)
)
@ -1294,7 +1294,7 @@ class CoordinateSystem:
colors = color_gradient(color, len(x_range_array))
for x, color in zip(x_range_array, colors, strict=False):
for x, color in zip(x_range_array, colors, strict=True):
if input_sample_type == "left":
sample_input = x
elif input_sample_type == "right":
@ -1608,7 +1608,7 @@ class CoordinateSystem:
x: float,
graph: ParametricFunction,
dx: float | None = None,
dx_line_color: ParsableManimColor = YELLOW,
dx_line_color: ParsableManimColor = PURE_YELLOW,
dy_line_color: ParsableManimColor | None = None,
dx_label: float | str | None = None,
dy_label: float | str | None = None,
@ -1790,7 +1790,7 @@ class CoordinateSystem:
triangle_size: float = MED_SMALL_BUFF,
triangle_color: ParsableManimColor | None = WHITE,
line_func: type[Line] = Line,
line_color: ParsableManimColor = YELLOW,
line_color: ParsableManimColor = PURE_YELLOW,
) -> VGroup:
"""Creates a labelled triangle marker with a vertical line from the x-axis
to a curve at a given x-value.
@ -2083,6 +2083,10 @@ class Axes(VGroup, CoordinateSystem):
``ax.coords_to_point( [[x_0, y_0, z_0], [x_1, y_1, z_1]] )``
A single coordinate can also be passed as a flat list or 1D array:
``ax.coords_to_point( [x, y, z] )``
Returns
-------
np.ndarray
@ -2111,6 +2115,10 @@ class Axes(VGroup, CoordinateSystem):
array([[0. , 0.86, 0.86],
[0.75, 0.75, 0. ],
[0. , 0. , 0. ]])
>>> np.around(ax.coords_to_point([1, 0, 0]), 2)
array([0.86, 0. , 0. ])
>>> np.around(ax.coords_to_point(np.array([1, 0])), 2)
array([0.86, 0. , 0. ])
.. manim:: CoordsToPointExample
:save_last_frame:
@ -2153,6 +2161,10 @@ class Axes(VGroup, CoordinateSystem):
else:
coords = coords.T
are_coordinates_transposed = True
# If coords is in the format ([x, y, z]) -- a single flat list/array passed as one argument:
elif coords.ndim == 2 and coords.shape[0] == 1:
# Extract the single list so [x, y, z] is treated like c2p(x, y, z).
coords = coords[0]
# Otherwise, coords already looked like (x, y, z) or ([x1 x2 ...], [y1 y2 ...], [z1 z2 ...]),
# so no further processing is needed.
@ -2287,7 +2299,7 @@ class Axes(VGroup, CoordinateSystem):
x_values: Iterable[float],
y_values: Iterable[float],
z_values: Iterable[float] | None = None,
line_color: ParsableManimColor = YELLOW,
line_color: ParsableManimColor = PURE_YELLOW,
add_vertex_dots: bool = True,
vertex_dot_radius: float = DEFAULT_DOT_RADIUS,
vertex_dot_style: dict[str, Any] | None = None,
@ -2356,7 +2368,7 @@ class Axes(VGroup, CoordinateSystem):
vertices = [
self.coords_to_point(x, y, z)
for x, y, z in zip(x_values, y_values, z_values, strict=False)
for x, y, z in zip(x_values, y_values, z_values, strict=True)
]
graph.set_points_as_corners(vertices)
line_graph["line_graph"] = graph

View file

@ -21,7 +21,7 @@ if TYPE_CHECKING:
from manim.typing import Point3D, Point3DLike
from manim.utils.color import ParsableManimColor
from manim.utils.color import YELLOW
from manim.utils.color import PURE_YELLOW
class ParametricFunction(VMobject):
@ -156,7 +156,7 @@ class ParametricFunction(VMobject):
else:
boundary_times = [self.t_min, self.t_max]
for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2], strict=False):
for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2], strict=True):
t_range = np.array(
[
*self.scaling.function(np.arange(t1, t2, self.t_step)),
@ -216,7 +216,7 @@ class FunctionGraph(ParametricFunction):
self,
function: Callable[[float], Any],
x_range: tuple[float, float] | tuple[float, float, float] | None = None,
color: ParsableManimColor = YELLOW,
color: ParsableManimColor = PURE_YELLOW,
**kwargs: Any,
) -> None:
if x_range is None:

View file

@ -265,7 +265,7 @@ class NumberLine(Line):
zip(
tick_range,
custom_labels,
strict=False,
strict=True,
)
),
)

View file

@ -111,7 +111,7 @@ class SampleSpace(Rectangle):
last_point = self.get_edge_center(-vect)
parts = VGroup()
for factor, color in zip(p_list_complete, colors_in_gradient, strict=False):
for factor, color in zip(p_list_complete, colors_in_gradient, strict=True):
part = SampleSpace()
part.set_fill(color, 1)
part.replace(self, stretch=True)
@ -372,7 +372,7 @@ class BarChart(Axes):
labels = VGroup()
for i, (value, bar_name) in enumerate(
zip(val_range, self.bar_names, strict=False)
zip(val_range, self.bar_names, strict=True)
):
# to accommodate negative bars, the label may need to be
# below or above the x_axis depending on the value of the bar

View file

@ -17,7 +17,7 @@ import warnings
from collections.abc import Callable, Iterable
from functools import partialmethod, reduce
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING, Any
import numpy as np
@ -28,8 +28,8 @@ from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.opengl.opengl_mobject import InvisibleMobject
from manim.utils.color import (
BLACK,
PURE_YELLOW,
WHITE,
YELLOW_C,
ManimColor,
ParsableManimColor,
color_gradient,
@ -395,6 +395,42 @@ class Mobject:
"""
return _AnimationBuilder(self)
@property
def always(self) -> Self:
"""Call a method on a mobject every frame.
This is syntactic sugar for ``mob.add_updater(lambda m: m.method(*args, **kwargs), call_updater=True)``.
Note that this will call the method immediately. If this behavior is not
desired, you should use :meth:`add_updater` directly.
.. warning::
Chaining of methods is allowed, but each method will be added
as its own updater. If you are chaining methods, make sure they
do not interfere with each other or you may get unexpected results.
.. warning::
:attr:`always` is not compatible with :meth:`.ValueTracker.get_value`, because
the value will be computed once and then never updated again. Use :meth:`add_updater`
if you would like to use a :class:`~.ValueTracker` to update the value.
Example
-------
.. manim:: AlwaysExample
class AlwaysExample(Scene):
def construct(self):
sq = Square().to_edge(LEFT)
t = Text("Hello World!")
t.always.next_to(sq, UP)
self.add(sq, t)
self.play(sq.animate.to_edge(RIGHT))
"""
# can't use typing.cast because Self is under TYPE_CHECKING
return _UpdaterBuilder(self) # type: ignore[return-value]
def __deepcopy__(self, clone_from_id) -> Self:
cls = self.__class__
result = cls.__new__(cls)
@ -407,9 +443,10 @@ class Mobject:
def __repr__(self) -> str:
return str(self.name)
def reset_points(self) -> None:
def reset_points(self) -> Self:
"""Sets :attr:`points` to be an empty array."""
self.points = np.zeros((0, self.dim))
return self
def init_colors(self) -> object:
"""Initializes the colors.
@ -813,7 +850,7 @@ class Mobject:
self.scale_to_fit_depth(value)
# Can't be staticmethod because of point_cloud_mobject.py
def get_array_attrs(self) -> list[Literal["points"]]:
def get_array_attrs(self) -> list[str]:
return ["points"]
def apply_over_attr_arrays(self, func: MultiMappingFunction) -> Self:
@ -1980,7 +2017,7 @@ class Mobject:
# Color functions
def set_color(
self, color: ParsableManimColor = YELLOW_C, family: bool = True
self, color: ParsableManimColor = PURE_YELLOW, family: bool = True
) -> Self:
"""Condition is function which takes in one arguments, (x, y, z).
Here it just recurses to submobjects, but in subclasses this
@ -2031,7 +2068,7 @@ class Mobject:
mobs = self.family_members_with_points()
new_colors = color_gradient(colors, len(mobs))
for mob, color in zip(mobs, new_colors, strict=False):
for mob, color in zip(mobs, new_colors, strict=True):
mob.set_color(color, family=False)
return self
@ -2324,7 +2361,7 @@ class Mobject:
return Group(
*(
template.copy().pointwise_become_partial(self, a1, a2)
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=False)
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
)
)
@ -2535,7 +2572,7 @@ class Mobject:
x = VGroup(s1, s2, s3, s4).set_x(0).arrange(buff=1.0)
self.add(x)
"""
for m1, m2 in zip(self.submobjects, self.submobjects[1:], strict=False):
for m1, m2 in zip(self.submobjects[:-1], self.submobjects[1:], strict=True):
m2.next_to(m1, direction, buff, **kwargs)
if center:
self.center()
@ -2920,7 +2957,7 @@ class Mobject:
if not skip_point_alignment:
self.align_points(mobject)
# Recurse
for m1, m2 in zip(self.submobjects, mobject.submobjects, strict=False):
for m1, m2 in zip(self.submobjects, mobject.submobjects, strict=True):
m1.align_data(m2)
def get_point_mobject(self, center=None):
@ -2990,7 +3027,7 @@ class Mobject:
repeat_indices = (np.arange(target) * curr) // target
split_factors = [sum(repeat_indices == i) for i in range(curr)]
new_submobs = []
for submob, sf in zip(self.submobjects, split_factors, strict=False):
for submob, sf in zip(self.submobjects, split_factors, strict=True):
new_submobs.append(submob)
new_submobs.extend(submob.copy().fade(1) for _ in range(1, sf))
self.submobjects = new_submobs
@ -3202,7 +3239,7 @@ class Mobject:
mobject.move_to(self.get_center())
self.align_data(mobject, skip_point_alignment=True)
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=False):
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=True):
sm1.points = np.array(sm2.points)
sm1.interpolate_color(sm1, sm2, 1)
return self
@ -3411,6 +3448,24 @@ class _AnimationBuilder:
return anim
class _UpdaterBuilder:
"""Syntactic sugar for adding updaters to mobjects."""
def __init__(self, mobject: Mobject):
self._mobject = mobject
def __getattr__(self, name: str, /) -> Callable[..., Self]:
# just return a function that will add the updater
def add_updater(*method_args, **method_kwargs) -> Self:
self._mobject.add_updater(
lambda m: getattr(m, name)(*method_args, **method_kwargs),
call_updater=True,
)
return self
return add_updater
def override_animate(method) -> types.FunctionType:
r"""Decorator for overriding method animations.

View file

@ -9,13 +9,13 @@ import numpy as np
from manim.constants import ORIGIN, RIGHT, UP
from manim.mobject.opengl.opengl_point_cloud_mobject import OpenGLPMobject
from manim.typing import Point3DLike
from manim.utils.color import YELLOW, ParsableManimColor
from manim.utils.color import PURE_YELLOW, ParsableManimColor
class DotCloud(OpenGLPMobject):
def __init__(
self,
color: ParsableManimColor = YELLOW,
color: ParsableManimColor = PURE_YELLOW,
stroke_width: float = 2.0,
radius: float = 2.0,
density: float = 10,

View file

@ -1,6 +1,7 @@
from __future__ import annotations
from abc import ABCMeta
from typing import Any
from manim import config
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
@ -19,13 +20,15 @@ class ConvertToOpenGL(ABCMeta):
on the lowest order inheritance classes such as Mobject and VMobject.
"""
_converted_classes = []
_converted_classes: list[type] = []
def __new__(mcls, name, bases, namespace):
def __new__(
mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]
) -> type:
if config.renderer == RendererType.OPENGL:
# Must check class names to prevent
# cyclic importing.
base_names_to_opengl = {
base_names_to_opengl: dict[str, type] = {
"Mobject": OpenGLMobject,
"VMobject": OpenGLVMobject,
"PMobject": OpenGLPMobject,
@ -40,6 +43,6 @@ class ConvertToOpenGL(ABCMeta):
return super().__new__(mcls, name, bases, namespace)
def __init__(cls, name, bases, namespace):
def __init__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]):
super().__init__(name, bases, namespace)
cls._converted_classes.append(cls)

View file

@ -5,6 +5,7 @@ __all__ = [
]
from pathlib import Path
from typing import TYPE_CHECKING, Any
import numpy as np
from PIL import Image
@ -13,26 +14,29 @@ from PIL.Image import Resampling
from manim.mobject.opengl.opengl_surface import OpenGLSurface, OpenGLTexturedSurface
from manim.utils.images import get_full_raster_image_path
if TYPE_CHECKING:
import numpy.typing as npt
__all__ = ["OpenGLImageMobject"]
class OpenGLImageMobject(OpenGLTexturedSurface):
def __init__(
self,
filename_or_array: str | Path | np.ndarray,
width: float = None,
height: float = None,
filename_or_array: str | Path | npt.NDArray,
width: float | None = None,
height: float | None = None,
image_mode: str = "RGBA",
resampling_algorithm: int = Resampling.BICUBIC,
resampling_algorithm: Resampling = Resampling.BICUBIC,
opacity: float = 1,
gloss: float = 0,
shadow: float = 0,
**kwargs,
**kwargs: Any,
):
self.image = filename_or_array
self.resampling_algorithm = resampling_algorithm
if isinstance(filename_or_array, np.ndarray):
self.size = self.image.shape[1::-1]
self.size = filename_or_array.shape[1::-1]
elif isinstance(filename_or_array, (str, Path)):
path = get_full_raster_image_path(filename_or_array)
self.size = Image.open(path).size
@ -68,7 +72,7 @@ class OpenGLImageMobject(OpenGLTexturedSurface):
self,
image_file: str | Path | np.ndarray,
image_mode: str,
):
) -> Image.Image:
if isinstance(image_file, (str, Path)):
return super().get_image_from_file(image_file, image_mode)
else:
@ -76,7 +80,7 @@ class OpenGLImageMobject(OpenGLTexturedSurface):
Image.fromarray(image_file.astype("uint8"))
.convert(image_mode)
.resize(
np.array(image_file.shape[:2])
image_file.shape[:2]
* 200, # assumption of 200 ppmu (pixels per manim unit) would suffice
resample=self.resampling_algorithm,
)

View file

@ -1136,7 +1136,7 @@ class OpenGLMobject:
x = OpenGLVGroup(s1, s2, s3, s4).set_x(0).arrange(buff=1.0)
self.add(x)
"""
for m1, m2 in zip(self.submobjects, self.submobjects[1:], strict=False):
for m1, m2 in zip(self.submobjects[:-1], self.submobjects[1:], strict=True):
m2.next_to(m1, direction, **kwargs)
if center:
self.center()
@ -2415,7 +2415,7 @@ class OpenGLMobject:
mobs = self.submobjects
new_colors = color_gradient(colors, len(mobs))
for mob, color in zip(mobs, new_colors, strict=False):
for mob, color in zip(mobs, new_colors, strict=True):
mob.set_color(color)
return self
@ -2638,7 +2638,7 @@ class OpenGLMobject:
return OpenGLGroup(
*(
template.copy().pointwise_become_partial(self, a1, a2)
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=False)
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
)
)
@ -2774,7 +2774,7 @@ class OpenGLMobject:
mob1.add_n_more_submobjects(max(0, n2 - n1))
mob2.add_n_more_submobjects(max(0, n1 - n2))
# Recurse
for sm1, sm2 in zip(mob1.submobjects, mob2.submobjects, strict=False):
for sm1, sm2 in zip(mob1.submobjects, mob2.submobjects, strict=True):
sm1.align_family(sm2)
return self
@ -2801,7 +2801,7 @@ class OpenGLMobject:
repeat_indices = (np.arange(target) * curr) // target
split_factors = [(repeat_indices == i).sum() for i in range(curr)]
new_submobs = []
for submob, sf in zip(self.submobjects, split_factors, strict=False):
for submob, sf in zip(self.submobjects, split_factors, strict=True):
new_submobs.append(submob)
for _ in range(1, sf):
new_submob = submob.copy()

View file

@ -11,8 +11,8 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.utils.bezier import interpolate
from manim.utils.color import (
BLACK,
PURE_YELLOW,
WHITE,
YELLOW,
ParsableManimColor,
color_gradient,
color_to_rgba,
@ -39,7 +39,7 @@ class OpenGLPMobject(OpenGLMobject):
def __init__(
self,
stroke_width: float = 2.0,
color: ParsableManimColor = YELLOW,
color: ParsableManimColor = PURE_YELLOW,
**kwargs,
):
self.stroke_width = stroke_width
@ -69,7 +69,7 @@ class OpenGLPMobject(OpenGLMobject):
Rgbas must be a Nx4 numpy array if it is not None.
"""
if rgbas is None and color is None:
color = YELLOW
color = PURE_YELLOW
self.append_points(points)
# rgbas array will have been resized with points
if color is not None:

View file

@ -2,19 +2,24 @@ from __future__ import annotations
from collections.abc import Iterable
from pathlib import Path
from typing import TYPE_CHECKING
import numpy as np
from PIL import Image
from manim.constants import *
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.typing import Point3D_Array, Vector3D_Array
from manim.utils.bezier import integer_interpolate, interpolate
from manim.utils.color import *
from manim.utils.images import change_to_rgba_array, get_full_raster_image_path
from manim.utils.iterables import listify
from manim.utils.space_ops import normalize_along_axis
if TYPE_CHECKING:
import numpy.typing as npt
from manim.typing import Point3D_Array, Vector3D_Array
__all__ = ["OpenGLSurface", "OpenGLTexturedSurface"]
@ -72,7 +77,7 @@ class OpenGLSurface(OpenGLMobject):
# can crop up in the shaders.
epsilon=1e-5,
depth_test=True,
**kwargs,
**kwargs: Any,
):
self.passed_uv_func = uv_func
self.u_range = u_range if u_range is not None else (0, 1)
@ -249,7 +254,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
def __init__(
self,
uv_surface: OpenGLSurface,
image_file: str | Path,
image_file: str | Path | npt.NDArray,
dark_image_file: str | Path = None,
image_mode: str | Iterable[str] = "RGBA",
**kwargs,
@ -289,7 +294,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
self,
image_file: str | Path,
image_mode: str,
):
) -> Image.Image:
image_file = get_full_raster_image_path(image_file)
return Image.open(image_file).convert(image_mode)

View file

@ -310,7 +310,7 @@ class OpenGLVMobject(OpenGLMobject):
return self
elif len(submobs2) == 0:
submobs2 = [vmobject]
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=False):
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=True):
sm1.match_style(sm2)
return self
@ -493,7 +493,6 @@ class OpenGLVMobject(OpenGLMobject):
self.set_points([point])
return self
end = self.points[-1]
alphas = np.linspace(0, 1, self.n_points_per_curve)
if self.long_lines:
halfway = interpolate(end, point, 0.5)
points = [interpolate(end, halfway, t) for t in self._bezier_t_values] + [
@ -663,9 +662,8 @@ class OpenGLVMobject(OpenGLMobject):
OpenGLVMobject
For chaining purposes.
"""
assert (
mode in ("jagged", "approx_smooth", "true_smooth"),
'mode must be either "jagged", "approx_smooth" or "smooth"',
assert mode in ("jagged", "approx_smooth", "true_smooth"), (
'mode must be either "jagged", "approx_smooth" or "smooth"'
)
nppc = self.n_points_per_curve
for submob in self.family_members_with_points():
@ -1131,7 +1129,7 @@ class OpenGLVMobject(OpenGLMobject):
s = self.get_start_anchors()
e = self.get_end_anchors()
return list(it.chain.from_iterable(zip(s, e, strict=False)))
return list(it.chain.from_iterable(zip(s, e, strict=True)))
def get_points_without_null_curves(self, atol: float = 1e-9) -> Point3D_Array:
nppc = self.n_points_per_curve

View file

@ -11,6 +11,7 @@ import numpy as np
import svgelements as se
from manim import config, logger
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
from ...constants import RIGHT
@ -24,7 +25,7 @@ from ..geometry.polygram import Polygon, Rectangle, RoundedRectangle
__all__ = ["SVGMobject", "VMobjectFromSVGPath"]
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
SVG_HASH_TO_MOB_MAP: dict[int, SVGMobject] = {}
def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
@ -112,6 +113,7 @@ class SVGMobject(VMobject):
self.svg_height = height
self.svg_width = width
self.opacity = opacity
self.id_to_vgroup_dict: dict[str, VGroup] = {}
if svg_default is None:
svg_default = {
@ -149,6 +151,7 @@ class SVGMobject(VMobject):
if hash_val in SVG_HASH_TO_MOB_MAP:
mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
self.add(*mob)
self.id_to_vgroup_dict = mob.id_to_vgroup_dict
return
self.generate_mobject()
@ -182,8 +185,9 @@ class SVGMobject(VMobject):
svg = se.SVG.parse(modified_file_path)
modified_file_path.unlink()
mobjects = self.get_mobjects_from(svg)
mobjects, mobject_dict = self.get_mobjects_from(svg)
self.add(*mobjects)
self.id_to_vgroup_dict = mobject_dict
self.flip(RIGHT) # Flip y
def get_file_path(self) -> Path:
@ -237,7 +241,9 @@ class SVGMobject(VMobject):
result[svg_key] = str(svg_default_dict[style_key])
return result
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
def get_mobjects_from(
self, svg: se.SVG
) -> tuple[list[VMobject], dict[str, VGroup]]:
"""Convert the elements of the SVG to a list of mobjects.
Parameters
@ -246,36 +252,77 @@ class SVGMobject(VMobject):
The parsed SVG file.
"""
result: list[VMobject] = []
for shape in svg.elements():
# can we combine the two continue cases into one?
if isinstance(shape, se.Group): # noqa: SIM114
continue
elif isinstance(shape, se.Path):
mob: VMobject = self.path_to_mobject(shape)
elif isinstance(shape, se.SimpleLine):
mob = self.line_to_mobject(shape)
elif isinstance(shape, se.Rect):
mob = self.rect_to_mobject(shape)
elif isinstance(shape, (se.Circle, se.Ellipse)):
mob = self.ellipse_to_mobject(shape)
elif isinstance(shape, se.Polygon):
mob = self.polygon_to_mobject(shape)
elif isinstance(shape, se.Polyline):
mob = self.polyline_to_mobject(shape)
elif isinstance(shape, se.Text):
mob = self.text_to_mobject(shape)
elif isinstance(shape, se.Use) or type(shape) is se.SVGElement:
continue
else:
logger.warning(f"Unsupported element type: {type(shape)}")
continue
if mob is None or not mob.has_points():
continue
self.apply_style_to_mobject(mob, shape)
if isinstance(shape, se.Transformable) and shape.apply:
self.handle_transform(mob, shape.transform)
result.append(mob)
return result
stack: list[tuple[se.SVGElement, int]] = []
stack.append((svg, 1))
group_id_number = 0
vgroup_stack: list[str] = ["root"]
vgroup_names: list[str] = ["root"]
vgroups: dict[str, VGroup] = {"root": VGroup()}
while len(stack) > 0:
element, depth = stack.pop()
# Reduce stack heights
vgroup_stack = vgroup_stack[0:(depth)]
try:
group_name = str(element.values["id"])
except Exception:
group_name = f"numbered_group_{group_id_number}"
group_id_number += 1
vg = VGroup()
vgroup_names.append(group_name)
vgroup_stack.append(group_name)
vgroups[group_name] = vg
if isinstance(element, (se.Group, se.Use)):
stack.extend((subelement, depth + 1) for subelement in element[::-1])
# Add element to the parent vgroup
try:
if isinstance(
element,
(
se.Path,
se.SimpleLine,
se.Rect,
se.Circle,
se.Ellipse,
se.Polygon,
se.Polyline,
se.Text,
),
):
mob = self.get_mob_from_shape_element(element)
if mob is not None:
result.append(mob)
for parent_name in vgroup_stack[:-1]:
vgroups[parent_name].add(mob)
except Exception as e:
logger.error(f"Exception occurred in 'get_mobjects_from'. Details: {e}")
return result, vgroups
def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None:
if isinstance(shape, se.Path):
mob: VMobject | None = self.path_to_mobject(shape)
elif isinstance(shape, se.SimpleLine):
mob = self.line_to_mobject(shape)
elif isinstance(shape, se.Rect):
mob = self.rect_to_mobject(shape)
elif isinstance(shape, (se.Circle, se.Ellipse)):
mob = self.ellipse_to_mobject(shape)
elif isinstance(shape, se.Polygon):
mob = self.polygon_to_mobject(shape)
elif isinstance(shape, se.Polyline):
mob = self.polyline_to_mobject(shape)
elif isinstance(shape, se.Text):
mob = self.text_to_mobject(shape)
else:
logger.warning(f"Unsupported element type: {type(shape)}")
mob = None
if mob is None or not mob.has_points():
return mob
self.apply_style_to_mobject(mob, shape)
if isinstance(shape, se.Transformable) and shape.apply:
self.handle_transform(mob, shape.transform)
return mob
@staticmethod
def handle_transform(mob: VMobject, matrix: se.Matrix) -> VMobject:

View file

@ -84,7 +84,7 @@ from ..animation.animation import Animation
from ..animation.composition import AnimationGroup
from ..animation.creation import Create, Write
from ..animation.fading import FadeIn
from ..utils.color import BLACK, YELLOW, ManimColor, ParsableManimColor
from ..utils.color import BLACK, PURE_YELLOW, ManimColor, ParsableManimColor
class Table(VGroup):
@ -814,7 +814,10 @@ class Table(VGroup):
return rec
def get_highlighted_cell(
self, pos: Sequence[int] = (1, 1), color: ParsableManimColor = YELLOW, **kwargs
self,
pos: Sequence[int] = (1, 1),
color: ParsableManimColor = PURE_YELLOW,
**kwargs,
) -> BackgroundRectangle:
"""Returns a :class:`~.BackgroundRectangle` of the cell at the given position.
@ -850,7 +853,10 @@ class Table(VGroup):
return bg_cell
def add_highlighted_cell(
self, pos: Sequence[int] = (1, 1), color: ParsableManimColor = YELLOW, **kwargs
self,
pos: Sequence[int] = (1, 1),
color: ParsableManimColor = PURE_YELLOW,
**kwargs,
) -> Table:
"""Highlights one cell at a specific position on the table by adding a :class:`~.BackgroundRectangle`.
@ -1081,11 +1087,11 @@ class IntegerTable(Table):
[[0,30,45,60,90],
[90,60,45,30,0]],
col_labels=[
MathTex(r"\frac{\sqrt{0}}{2}"),
MathTex(r"\frac{\sqrt{1}}{2}"),
MathTex(r"\frac{\sqrt{2}}{2}"),
MathTex(r"\frac{\sqrt{3}}{2}"),
MathTex(r"\frac{\sqrt{4}}{2}")],
MathTex(r"\frac{ \sqrt{0} }{2}"),
MathTex(r"\frac{ \sqrt{1} }{2}"),
MathTex(r"\frac{ \sqrt{2} }{2}"),
MathTex(r"\frac{ \sqrt{3} }{2}"),
MathTex(r"\frac{ \sqrt{4} }{2}")],
row_labels=[MathTex(r"\sin"), MathTex(r"\cos")],
h_buff=1,
element_to_mobject_config={"unit": r"^{\circ}"})

View file

@ -12,7 +12,7 @@ r"""Mobjects representing text rendered using LaTeX.
from __future__ import annotations
from manim.utils.color import BLACK, ManimColor, ParsableManimColor
from manim.utils.color import BLACK, ParsableManimColor
__all__ = [
"SingleStringMathTex",
@ -23,10 +23,9 @@ __all__ = [
]
import itertools as it
import operator as op
import re
from collections.abc import Iterable, Sequence
from collections.abc import Iterable
from functools import reduce
from textwrap import dedent
from typing import Any, Self
@ -35,10 +34,13 @@ from manim import config, logger
from manim.constants import *
from manim.mobject.geometry.line import Line
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
from manim.mobject.svg.svg_mobject import SVGMobject
from manim.utils.tex import TexTemplate
from manim.utils.tex_file_writing import tex_to_svg_file
MATHTEX_SUBSTRING = "substring"
class SingleStringMathTex(SVGMobject):
"""Elementary building block for rendering text with LaTeX.
@ -261,22 +263,30 @@ class MathTex(SingleStringMathTex):
self.tex_template = kwargs.pop("tex_template", config["tex_template"])
self.arg_separator = arg_separator
self.substrings_to_isolate = (
[] if substrings_to_isolate is None else substrings_to_isolate
[] if substrings_to_isolate is None else list(substrings_to_isolate)
)
if tex_to_color_map is None:
self.tex_to_color_map: dict[str, ParsableManimColor] = {}
else:
self.tex_to_color_map = tex_to_color_map
self.substrings_to_isolate.extend(self.tex_to_color_map.keys())
self.tex_environment = tex_environment
self.brace_notation_split_occurred = False
self.tex_strings = self._break_up_tex_strings(tex_strings)
self.tex_strings = self._prepare_tex_strings(tex_strings)
self.matched_strings_and_ids: list[tuple[str, str]] = []
try:
joined_string = self._join_tex_strings_with_unique_deliminters(
self.tex_strings, self.substrings_to_isolate
)
super().__init__(
self.arg_separator.join(self.tex_strings),
joined_string,
tex_environment=self.tex_environment,
tex_template=self.tex_template,
**kwargs,
)
# Save the original tex_string
self.tex_string = self.arg_separator.join(self.tex_strings)
self._break_up_by_substrings()
except ValueError:
if self.brace_notation_split_occurred:
@ -298,36 +308,109 @@ class MathTex(SingleStringMathTex):
if self.organize_left_to_right:
self._organize_submobjects_left_to_right()
def _break_up_tex_strings(self, tex_strings: Sequence[str]) -> list[str]:
# Separate out anything surrounded in double braces
pre_split_length = len(tex_strings)
tex_strings_brace_splitted = [
re.split("{{(.*?)}}", str(t)) for t in tex_strings
def _prepare_tex_strings(self, tex_strings: Iterable[str]) -> list[str]:
# Deal with the case where tex_strings contains integers instead
# of strings.
tex_strings_validated = [
string if isinstance(string, str) else str(string) for string in tex_strings
]
tex_strings_combined = sum(tex_strings_brace_splitted, [])
if len(tex_strings_combined) > pre_split_length:
# Locate double curly bracers
tex_strings_validated_two = []
for tex_string in tex_strings_validated:
split = re.split(r"{{|}}", tex_string)
tex_strings_validated_two.extend(split)
if len(tex_strings_validated_two) > len(tex_strings_validated):
self.brace_notation_split_occurred = True
return [string for string in tex_strings_validated_two if len(string) > 0]
# Separate out any strings specified in the isolate
# or tex_to_color_map lists.
patterns = []
patterns.extend(
[
f"({re.escape(ss)})"
for ss in it.chain(
self.substrings_to_isolate,
self.tex_to_color_map.keys(),
def _join_tex_strings_with_unique_deliminters(
self, tex_strings: list[str], substrings_to_isolate: Iterable[str]
) -> str:
joined_string = ""
ssIdx = 0
for idx, tex_string in enumerate(tex_strings):
string_part = rf"\special{{dvisvgm:raw <g id='unique{idx:03d}'>}}"
self.matched_strings_and_ids.append((tex_string, f"unique{idx:03d}"))
# Try to match with all substrings_to_isolate and apply the first match
# then match again (on the rest of the string) and continue until no
# characters are left in the string
unprocessed_string = str(tex_string)
processed_string = ""
while len(unprocessed_string) > 0:
first_match = self._locate_first_match(
substrings_to_isolate, unprocessed_string
)
],
if first_match:
processed, unprocessed_string = self._handle_match(
ssIdx, first_match
)
processed_string = processed_string + processed
ssIdx += 1
else:
processed_string = processed_string + unprocessed_string
unprocessed_string = ""
string_part += processed_string
if idx < len(tex_strings) - 1:
string_part += self.arg_separator
string_part += r"\special{dvisvgm:raw </g>}"
joined_string = joined_string + string_part
return joined_string
def _locate_first_match(
self, substrings_to_isolate: Iterable[str], unprocessed_string: str
) -> re.Match | None:
first_match_start = len(unprocessed_string)
first_match_length = 0
first_match = None
for substring in substrings_to_isolate:
match = re.match(f"(.*?)({re.escape(substring)})(.*)", unprocessed_string)
if match and len(match.group(1)) < first_match_start:
first_match = match
first_match_start = len(match.group(1))
first_match_length = len(match.group(2))
elif match and len(match.group(1)) == first_match_start:
# Break ties by looking at length of matches.
if first_match_length < len(match.group(2)):
first_match = match
first_match_start = len(match.group(1))
first_match_length = len(match.group(2))
return first_match
def _handle_match(self, ssIdx: int, first_match: re.Match) -> tuple[str, str]:
pre_match = first_match.group(1)
matched_string = first_match.group(2)
post_match = first_match.group(3)
pre_string = (
rf"\special{{dvisvgm:raw <g id='unique{ssIdx:03d}{MATHTEX_SUBSTRING}'>}}"
)
pattern = "|".join(patterns)
if pattern:
pieces = []
for s in tex_strings_combined:
pieces.extend(re.split(pattern, s))
else:
pieces = tex_strings_combined
return [p for p in pieces if p]
post_string = r"\special{dvisvgm:raw </g>}"
self.matched_strings_and_ids.append(
(matched_string, f"unique{ssIdx:03d}{MATHTEX_SUBSTRING}")
)
processed_string = pre_match + pre_string + matched_string + post_string
unprocessed_string = post_match
return processed_string, unprocessed_string
@property
def _substring_matches(self) -> list[tuple[str, str]]:
"""Return only the 'ss' (substring_to_isolate) matches."""
return [
(tex, id_)
for tex, id_ in self.matched_strings_and_ids
if id_.endswith(MATHTEX_SUBSTRING)
]
@property
def _main_matches(self) -> list[tuple[str, str]]:
"""Return only the main tex_string matches."""
return [
(tex, id_)
for tex, id_ in self.matched_strings_and_ids
if not id_.endswith(MATHTEX_SUBSTRING)
]
def _break_up_by_substrings(self) -> Self:
"""
@ -336,25 +419,17 @@ class MathTex(SingleStringMathTex):
of tex_strings)
"""
new_submobjects: list[VMobject] = []
curr_index = 0
for tex_string in self.tex_strings:
sub_tex_mob = SingleStringMathTex(
tex_string,
tex_environment=self.tex_environment,
tex_template=self.tex_template,
try:
for tex_string, tex_string_id in self._main_matches:
mtp = MathTexPart()
mtp.tex_string = tex_string
mtp.add(*self.id_to_vgroup_dict[tex_string_id].submobjects)
new_submobjects.append(mtp)
except KeyError:
logger.error(
f"MathTex: Could not find SVG group for tex part '{tex_string}' (id: {tex_string_id}). Using fallback to root group."
)
num_submobs = len(sub_tex_mob.submobjects)
new_index = (
curr_index + num_submobs + len("".join(self.arg_separator.split()))
)
if num_submobs == 0:
last_submob_index = min(curr_index, len(self.submobjects) - 1)
sub_tex_mob.move_to(self.submobjects[last_submob_index], RIGHT)
else:
sub_tex_mob.submobjects = self.submobjects[curr_index:new_index]
sub_tex_mob.note_changed_family()
new_submobjects.append(sub_tex_mob)
curr_index = new_index
new_submobjects.append(self.id_to_vgroup_dict["root"])
self.submobjects = new_submobjects
# 5 hours of work went into this line
@ -364,30 +439,18 @@ class MathTex(SingleStringMathTex):
return self
def get_parts_by_tex(
self, tex: str, substring: bool = True, case_sensitive: bool = True
) -> VGroup:
def test(tex1: str, tex2: str) -> bool:
if not case_sensitive:
tex1 = tex1.lower()
tex2 = tex2.lower()
if substring:
return tex1 in tex2
else:
return tex1 == tex2
return VGroup(*(m for m in self.submobjects if test(tex, m.get_tex_string())))
def get_part_by_tex(self, tex: str, **kwargs: Any) -> MathTex | None:
all_parts = self.get_parts_by_tex(tex, **kwargs)
return all_parts[0] if all_parts else None
def get_part_by_tex(self, tex: str, **kwargs: Any) -> VGroup | None:
for tex_str, match_id in self.matched_strings_and_ids:
if tex_str == tex:
return self.id_to_vgroup_dict[match_id]
return None
def set_color_by_tex(
self, tex: str, color: ParsableManimColor, **kwargs: Any
) -> Self:
parts_to_color = self.get_parts_by_tex(tex, **kwargs)
for part in parts_to_color:
part.set_color(color)
for tex_str, match_id in self.matched_strings_and_ids:
if tex_str == tex:
self.id_to_vgroup_dict[match_id].set_color(color)
return self
def set_opacity_by_tex(
@ -413,22 +476,18 @@ class MathTex(SingleStringMathTex):
"""
if remaining_opacity is not None:
self.set_opacity(opacity=remaining_opacity)
for part in self.get_parts_by_tex(tex):
part.set_opacity(opacity)
for tex_str, match_id in self.matched_strings_and_ids:
if tex_str == tex:
self.id_to_vgroup_dict[match_id].set_opacity(opacity)
return self
def set_color_by_tex_to_color_map(
self, texs_to_color_map: dict[str, ParsableManimColor], **kwargs: Any
) -> Self:
for texs, color in list(texs_to_color_map.items()):
try:
# If the given key behaves like tex_strings
texs + ""
self.set_color_by_tex(texs, ManimColor(color), **kwargs)
except TypeError:
# If the given key is a tuple
for tex in texs:
self.set_color_by_tex(tex, ManimColor(color), **kwargs)
for match in self.matched_strings_and_ids:
if match[0] == texs:
self.id_to_vgroup_dict[match[1]].set_color(color)
return self
def index_of_part(self, part: MathTex) -> int:
@ -437,17 +496,18 @@ class MathTex(SingleStringMathTex):
raise ValueError("Trying to get index of part not in MathTex")
return split_self.index(part)
def index_of_part_by_tex(self, tex: str, **kwargs: Any) -> int:
part = self.get_part_by_tex(tex, **kwargs)
if part is None:
return -1
return self.index_of_part(part)
def sort_alphabetically(self) -> None:
self.submobjects.sort(key=lambda m: m.get_tex_string())
self.note_changed_family()
class MathTexPart(VMobject):
tex_string: str
def __repr__(self) -> str:
return f"{type(self).__name__}({repr(self.tex_string)})"
class Tex(MathTex):
r"""A string compiled with LaTeX in normal mode.

View file

@ -809,8 +809,7 @@ class Text(SVGMobject):
line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
dir_name = config.get_dir("text_dir")
if not dir_name.is_dir():
dir_name.mkdir(parents=True)
dir_name.mkdir(parents=True, exist_ok=True)
hash_name = self._text2hash(color)
file_name = dir_name / (hash_name + ".svg")
@ -1350,8 +1349,7 @@ class MarkupText(SVGMobject):
line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
dir_name = config.get_dir("text_dir")
if not dir_name.is_dir():
dir_name.mkdir(parents=True)
dir_name.mkdir(parents=True, exist_ok=True)
hash_name = self._text2hash(color)
file_name = dir_name / (hash_name + ".svg")

View file

@ -132,7 +132,7 @@ class Polyhedron(VGroup):
"""Creates list of cyclic pairwise tuples."""
edges: list[tuple[int, int]] = []
for face in faces_list:
edges += zip(face, face[1:] + face[:1], strict=False)
edges += zip(face, face[1:] + face[:1], strict=True)
return edges
def create_faces(

View file

@ -17,7 +17,13 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
from ... import config
from ...constants import *
from ...utils.bezier import interpolate
from ...utils.color import WHITE, ManimColor, color_to_int_rgb
from ...utils.color import (
WHITE,
YELLOW_C,
ManimColor,
ParsableManimColor,
color_to_int_rgb,
)
from ...utils.images import change_to_rgba_array, get_full_raster_image_path
__all__ = ["ImageMobject", "ImageMobjectFromCamera"]
@ -61,9 +67,14 @@ class AbstractImageMobject(Mobject):
def get_pixel_array(self) -> PixelArray:
raise NotImplementedError()
def set_color(self, color, alpha=None, family=True):
def set_color( # type: ignore[override]
self,
color: ParsableManimColor = YELLOW_C,
alpha: Any = None,
family: bool = True,
) -> AbstractImageMobject:
# Likely to be implemented in subclasses, but no obligation
pass
raise NotImplementedError()
def set_resampling_algorithm(self, resampling_algorithm: int) -> Self:
"""
@ -86,18 +97,18 @@ class AbstractImageMobject(Mobject):
* 'hamming'
* 'lanczos' or 'antialias'
"""
if isinstance(resampling_algorithm, int):
self.resampling_algorithm = resampling_algorithm
else:
if resampling_algorithm not in RESAMPLING_ALGORITHMS.values():
raise ValueError(
"resampling_algorithm has to be an int, one of the values defined in "
"RESAMPLING_ALGORITHMS or a Pillow resampling filter constant. "
"Available algorithms: 'bicubic', 'nearest', 'box', 'bilinear', "
"'hamming', 'lanczos'.",
"Available algorithms: 'bicubic' (or 'cubic'), 'nearest' (or 'none'), "
"'bilinear' (or 'linear').",
)
self.resampling_algorithm = resampling_algorithm
return self
def reset_points(self) -> None:
def reset_points(self) -> Self:
"""Sets :attr:`points` to be the four image corners."""
self.points = np.array(
[
@ -115,6 +126,7 @@ class AbstractImageMobject(Mobject):
height = 3 # this is the case for ImageMobjectFromCamera
self.stretch_to_fit_height(height)
self.stretch_to_fit_width(height * w / h)
return self
class ImageMobject(AbstractImageMobject):
@ -156,27 +168,18 @@ class ImageMobject(AbstractImageMobject):
[0, 0, 0, 255]
]))
img.height = 2
img1 = img.copy()
img2 = img.copy()
img3 = img.copy()
img4 = img.copy()
img5 = img.copy()
img.height = 3
img1.set_resampling_algorithm(RESAMPLING_ALGORITHMS["nearest"])
img2.set_resampling_algorithm(RESAMPLING_ALGORITHMS["lanczos"])
img3.set_resampling_algorithm(RESAMPLING_ALGORITHMS["linear"])
img4.set_resampling_algorithm(RESAMPLING_ALGORITHMS["cubic"])
img5.set_resampling_algorithm(RESAMPLING_ALGORITHMS["box"])
img1.add(Text("nearest").scale(0.5).next_to(img1,UP))
img2.add(Text("lanczos").scale(0.5).next_to(img2,UP))
img3.add(Text("linear").scale(0.5).next_to(img3,UP))
img4.add(Text("cubic").scale(0.5).next_to(img4,UP))
img5.add(Text("box").scale(0.5).next_to(img5,UP))
group = Group()
algorithm_texts = ["nearest", "linear", "cubic"]
for algorithm_text in algorithm_texts:
algorithm = RESAMPLING_ALGORITHMS[algorithm_text]
img_copy = img.copy().set_resampling_algorithm(algorithm)
img_copy.add(Text(algorithm_text).scale(0.5).next_to(img_copy, UP))
group.add(img_copy)
x= Group(img1,img2,img3,img4,img5)
x.arrange()
self.add(x)
group.arrange()
self.add(group)
"""
def __init__(
@ -209,18 +212,23 @@ class ImageMobject(AbstractImageMobject):
self.orig_alpha_pixel_array = self.pixel_array[:, :, 3].copy()
super().__init__(scale_to_resolution, **kwargs)
def get_pixel_array(self):
def get_pixel_array(self) -> PixelArray:
"""A simple getter method."""
return self.pixel_array
def set_color(self, color, alpha=None, family=True):
def set_color( # type: ignore[override]
self,
color: ParsableManimColor = YELLOW_C,
alpha: Any = None,
family: bool = True,
) -> Self:
rgb = color_to_int_rgb(color)
self.pixel_array[:, :, :3] = rgb
if alpha is not None:
self.pixel_array[:, :, 3] = int(255 * alpha)
for submob in self.submobjects:
submob.set_color(color, alpha, family)
self.color = color
self.color = ManimColor(color)
return self
def set_opacity(self, alpha: float) -> Self:
@ -252,7 +260,7 @@ class ImageMobject(AbstractImageMobject):
return self
def interpolate_color(
self, mobject1: ImageMobject, mobject2: ImageMobject, alpha: float
self, mobject1: Mobject, mobject2: Mobject, alpha: float
) -> None:
"""Interpolates the array of pixel color values from one ImageMobject
into an array of equal size in the target ImageMobject.
@ -268,6 +276,8 @@ class ImageMobject(AbstractImageMobject):
alpha
Used to track the lerp relationship. Not opacity related.
"""
assert isinstance(mobject1, ImageMobject)
assert isinstance(mobject2, ImageMobject)
assert mobject1.pixel_array.shape == mobject2.pixel_array.shape, (
f"Mobject pixel array shapes incompatible for interpolation.\n"
f"Mobject 1 ({mobject1}) : {mobject1.pixel_array.shape}\n"
@ -291,7 +301,7 @@ class ImageMobject(AbstractImageMobject):
def get_style(self) -> dict[str, Any]:
return {
"fill_color": ManimColor(self.color.get_rgb()).to_hex(),
"fill_color": ManimColor(self.color.to_rgb()).to_hex(),
"fill_opacity": self.fill_opacity,
}
@ -320,7 +330,7 @@ class ImageMobjectFromCamera(AbstractImageMobject):
super().__init__(scale_to_resolution=False, **kwargs)
# TODO: Get rid of this.
def get_pixel_array(self):
def get_pixel_array(self) -> PixelArray:
self.pixel_array = self.camera.pixel_array
return self.pixel_array
@ -331,7 +341,11 @@ class ImageMobjectFromCamera(AbstractImageMobject):
self.add(self.display_frame)
return self
def interpolate_color(self, mobject1, mobject2, alpha) -> None:
def interpolate_color(
self, mobject1: Mobject, mobject2: Mobject, alpha: float
) -> None:
assert isinstance(mobject1, ImageMobjectFromCamera)
assert isinstance(mobject2, ImageMobjectFromCamera)
assert mobject1.pixel_array.shape == mobject2.pixel_array.shape, (
f"Mobject pixel array shapes incompatible for interpolation.\n"
f"Mobject 1 ({mobject1}) : {mobject1.pixel_array.shape}\n"

View file

@ -16,8 +16,8 @@ from ...mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
from ...utils.bezier import interpolate
from ...utils.color import (
BLACK,
PURE_YELLOW,
WHITE,
YELLOW,
ManimColor,
ParsableManimColor,
color_gradient,
@ -109,7 +109,7 @@ class PMobject(Mobject):
return self
def set_color(
self, color: ParsableManimColor = YELLOW, family: bool = True
self, color: ParsableManimColor = PURE_YELLOW, family: bool = True
) -> Self:
rgba = color_to_rgba(color)
mobs = self.family_members_with_points() if family else [self]
@ -129,7 +129,7 @@ class PMobject(Mobject):
def set_color_by_gradient(self, *colors: ParsableManimColor) -> Self:
self.rgbas = np.array(
list(map(color_to_rgba, color_gradient(*colors, len(self.points)))),
list(map(color_to_rgba, color_gradient(colors, len(self.points)))),
)
return self
@ -171,7 +171,7 @@ class PMobject(Mobject):
for mob in self.family_members_with_points():
num_points = self.get_num_points()
mob.apply_over_attr_arrays(
lambda arr, n=num_points: arr[np.arange(0, n, factor)],
lambda arr, n=num_points: arr[np.arange(0, n, factor)], # type: ignore[misc]
)
return self
@ -181,7 +181,7 @@ class PMobject(Mobject):
"""Function is any map from R^3 to R"""
for mob in self.family_members_with_points():
indices = np.argsort(np.apply_along_axis(function, 1, mob.points))
mob.apply_over_attr_arrays(lambda arr, idx=indices: arr[idx])
mob.apply_over_attr_arrays(lambda arr, idx=indices: arr[idx]) # type: ignore[misc]
return self
def fade_to(
@ -198,7 +198,7 @@ class PMobject(Mobject):
def ingest_submobjects(self) -> Self:
attrs = self.get_array_attrs()
arrays = list(map(self.get_merged_array, attrs))
for attr, array in zip(attrs, arrays, strict=False):
for attr, array in zip(attrs, arrays, strict=True):
setattr(self, attr, array)
self.submobjects = []
self.note_changed_family()
@ -359,7 +359,7 @@ class PointCloudDot(Mobject1D):
radius: float = 2.0,
stroke_width: int = 2,
density: int = DEFAULT_POINT_DENSITY_1D,
color: ManimColor = YELLOW,
color: ManimColor = PURE_YELLOW,
**kwargs: Any,
) -> None:
self.radius = radius

View file

@ -238,7 +238,7 @@ class VMobject(Mobject):
rgbas: FloatRGBA_Array = np.array(
[
c.to_rgba_with_alpha(o)
for c, o in zip(*make_even(colors, opacities), strict=False)
for c, o in zip(*make_even(colors, opacities), strict=True)
],
)
@ -461,7 +461,7 @@ class VMobject(Mobject):
return self
elif len(submobs2) == 0:
submobs2 = [vmobject]
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=False):
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=True):
sm1.match_style(sm2)
return self
@ -1349,7 +1349,7 @@ class VMobject(Mobject):
split_indices = [0] + list(filtered) + [len(points)]
return (
points[i1:i2]
for i1, i2 in zip(split_indices, split_indices[1:], strict=False)
for i1, i2 in zip(split_indices[:-1], split_indices[1:], strict=True)
if (i2 - i1) >= nppcc
)
@ -1703,7 +1703,7 @@ class VMobject(Mobject):
s = self.get_start_anchors()
e = self.get_end_anchors()
return list(it.chain.from_iterable(zip(s, e, strict=False)))
return list(it.chain.from_iterable(zip(s, e, strict=True)))
def get_points_defining_boundary(self) -> Point3D_Array:
# Probably returns all anchors, but this is weird regarding the name of the method.

0
manim/py.typed Normal file
View file

View file

@ -44,9 +44,9 @@ from ..utils.color import (
BLUE_D,
GREEN_C,
GREY,
PURE_YELLOW,
RED_C,
WHITE,
YELLOW,
ManimColor,
ParsableManimColor,
)
@ -181,7 +181,7 @@ class VectorScene(Scene):
def add_vector(
self,
vector: Arrow | Vector3DLike,
color: ParsableManimColor | Iterable[ParsableManimColor] = YELLOW,
color: ParsableManimColor | Iterable[ParsableManimColor] = PURE_YELLOW,
animate: bool = True,
**kwargs: Any,
) -> Arrow:
@ -817,7 +817,7 @@ class LinearTransformationScene(VectorScene):
def get_unit_square(
self,
color: ParsableManimColor | Iterable[ParsableManimColor] = YELLOW,
color: ParsableManimColor | Iterable[ParsableManimColor] = PURE_YELLOW,
opacity: float = 0.3,
stroke_width: float = 3,
) -> Rectangle:
@ -884,7 +884,7 @@ class LinearTransformationScene(VectorScene):
def add_vector(
self,
vector: Arrow | list | tuple | np.ndarray,
color: ParsableManimColor = YELLOW,
color: ParsableManimColor = PURE_YELLOW,
animate: bool = False,
**kwargs: Any,
) -> Arrow:

View file

@ -1000,7 +1000,7 @@ def bezier_remap(
new_tuples = np.empty((new_number_of_curves, nppc, dim))
index = 0
for curve, sf in zip(bezier_tuples, split_factors, strict=False):
for curve, sf in zip(bezier_tuples, split_factors, strict=True):
new_tuples[index : index + sf] = subdivide_bezier(curve, sf).reshape(
sf, nppc, dim
)

View file

@ -1429,7 +1429,7 @@ def color_gradient(
floors[-1] = num_colors - 2
return [
rgb_to_color((rgbs[i] * (1 - alpha)) + (rgbs[i + 1] * alpha))
for i, alpha in zip(floors, alphas_mod1, strict=False)
for i, alpha in zip(floors, alphas_mod1, strict=True)
]
@ -1531,7 +1531,7 @@ class RandomColorGenerator:
>>> rnd = RandomColorGenerator(42)
>>> rnd.next()
ManimColor('#ECE7E2')
ManimColor('#8B4513')
>>> rnd.next()
ManimColor('#BBBBBB')
>>> rnd.next()
@ -1541,7 +1541,7 @@ class RandomColorGenerator:
>>> rnd2 = RandomColorGenerator(42)
>>> rnd2.next()
ManimColor('#ECE7E2')
ManimColor('#8B4513')
>>> rnd2.next()
ManimColor('#BBBBBB')
>>> rnd2.next()

View file

@ -102,6 +102,9 @@ These colors form Manim's default color space.
"pure_red",
"pure_green",
"pure_blue",
"pure_cyan",
"pure_magenta",
"pure_yellow",
)
pure_lines = named_lines_group(
@ -145,12 +148,17 @@ DARK_GRAY = ManimColor("#444444")
DARK_GREY = ManimColor("#444444")
DARKER_GRAY = ManimColor("#222222")
DARKER_GREY = ManimColor("#222222")
PURE_RED = ManimColor("#FF0000")
PURE_GREEN = ManimColor("#00FF00")
PURE_BLUE = ManimColor("#0000FF")
PURE_CYAN = ManimColor("#00FFFF")
PURE_MAGENTA = ManimColor("#FF00FF")
PURE_YELLOW = ManimColor("#FFFF00")
BLUE_A = ManimColor("#C7E9F1")
BLUE_B = ManimColor("#9CDCEB")
BLUE_C = ManimColor("#58C4DD")
BLUE_D = ManimColor("#29ABCA")
BLUE_E = ManimColor("#236B8E")
PURE_BLUE = ManimColor("#0000FF")
BLUE = ManimColor("#58C4DD")
DARK_BLUE = ManimColor("#236B8E")
TEAL_A = ManimColor("#ACEAD7")
@ -164,14 +172,13 @@ GREEN_B = ManimColor("#A6CF8C")
GREEN_C = ManimColor("#83C167")
GREEN_D = ManimColor("#77B05D")
GREEN_E = ManimColor("#699C52")
PURE_GREEN = ManimColor("#00FF00")
GREEN = ManimColor("#83C167")
YELLOW_A = ManimColor("#FFF1B6")
YELLOW_B = ManimColor("#FFEA94")
YELLOW_C = ManimColor("#FFFF00")
YELLOW_C = ManimColor("#F7D96F")
YELLOW_D = ManimColor("#F4D345")
YELLOW_E = ManimColor("#E8C11C")
YELLOW = ManimColor("#FFFF00")
YELLOW = ManimColor("#F7D96F")
GOLD_A = ManimColor("#F7C797")
GOLD_B = ManimColor("#F9B775")
GOLD_C = ManimColor("#F0AC5F")
@ -183,7 +190,6 @@ RED_B = ManimColor("#FF8080")
RED_C = ManimColor("#FC6255")
RED_D = ManimColor("#E65A4C")
RED_E = ManimColor("#CF5044")
PURE_RED = ManimColor("#FF0000")
RED = ManimColor("#FC6255")
MAROON_A = ManimColor("#ECABC1")
MAROON_B = ManimColor("#EC92AB")

View file

@ -153,15 +153,14 @@ def add_version_before_extension(file_name: Path) -> Path:
def guarantee_existence(path: Path) -> Path:
if not path.exists():
path.mkdir(parents=True)
path.mkdir(parents=True, exist_ok=True)
return path.resolve(strict=True)
def guarantee_empty_existence(path: Path) -> Path:
if path.exists():
shutil.rmtree(str(path))
path.mkdir(parents=True)
path.mkdir(parents=True, exist_ok=True)
return path.resolve(strict=True)

View file

@ -58,7 +58,7 @@ def adjacent_n_tuples(objects: Sequence[T], n: int) -> zip[tuple[T, ...]]:
>>> list(adjacent_n_tuples([1, 2, 3, 4], 3))
[(1, 2, 3), (2, 3, 4), (3, 4, 1), (4, 1, 2)]
"""
return zip(*([*objects[k:], *objects[:k]] for k in range(n)), strict=False)
return zip(*([*objects[k:], *objects[:k]] for k in range(n)), strict=True)
def adjacent_pairs(objects: Sequence[T]) -> zip[tuple[T, ...]]:

View file

@ -9,13 +9,12 @@ __all__ = [
"sigmoid",
]
import math
from collections.abc import Callable
from functools import lru_cache
from typing import Any, Protocol, TypeVar
import numpy as np
from scipy import special
def binary_search(
@ -87,9 +86,9 @@ def choose(n: int, k: int) -> int:
References
----------
- https://en.wikipedia.org/wiki/Combination
- https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.comb.html
- https://docs.python.org/3/library/math.html#math.comb
"""
value: int = special.comb(n, k, exact=True)
value: int = math.comb(n, k)
return value

View file

@ -625,7 +625,7 @@ def find_intersection(
# algorithm from https://en.wikipedia.org/wiki/Skew_lines#Nearest_points
result = []
for p0, v0, p1, v1 in zip(p0s, v0s, p1s, v1s, strict=False):
for p0, v0, p1, v1 in zip(p0s, v0s, p1s, v1s, strict=True):
normal = cross(v1, cross(v0, v1))
denom = max(np.dot(v0, normal), threshold)
result += [p0 + np.dot(p1 - p0, normal) / denom * v0]
@ -747,7 +747,10 @@ def earclip_triangulation(verts: np.ndarray, ring_ends: list) -> list:
list
A list of indices giving a triangulation of a polygon.
"""
rings = [list(range(e0, e1)) for e0, e1 in zip([0, *ring_ends], ring_ends)]
rings = [
list(range(e0, e1))
for e0, e1 in zip([0, *ring_ends[:-1]], ring_ends, strict=True)
]
def is_in(point, ring_id):
return (
@ -758,7 +761,7 @@ def earclip_triangulation(verts: np.ndarray, ring_ends: list) -> list:
def ring_area(ring_id):
ring = rings[ring_id]
s = 0
for i, j in zip(ring[1:], ring):
for i, j in zip(ring[1:], ring[:-1], strict=True):
s += cross2d(verts[i], verts[j])
return abs(s) / 2

View file

@ -103,8 +103,7 @@ def generate_tex_file(
output = tex_template.get_texcode_for_expression(expression)
tex_dir = config.get_dir("tex_dir")
if not tex_dir.exists():
tex_dir.mkdir()
tex_dir.mkdir(parents=True, exist_ok=True)
result = tex_dir / (tex_hash(output) + ".tex")
if not result.exists():

112
mypy.ini
View file

@ -67,21 +67,6 @@ ignore_errors = True
[mypy-manim.animation.creation]
ignore_errors = True
[mypy-manim.animation.fading]
ignore_errors = True
[mypy-manim.animation.growing]
ignore_errors = True
[mypy-manim.animation.indication]
ignore_errors = True
[mypy-manim.animation.movement]
ignore_errors = True
[mypy-manim.animation.numbers]
ignore_errors = True
[mypy-manim.animation.speedmodifier]
ignore_errors = True
@ -94,31 +79,7 @@ ignore_errors = True
[mypy-manim.animation.updaters.mobject_update_utils]
ignore_errors = True
[mypy-manim.camera.camera]
ignore_errors = True
[mypy-manim.cli.checkhealth.commands]
ignore_errors = True
[mypy-manim.manager]
ignore_errors = True
[mypy-manim.mobject.frame]
ignore_errors = True
[mypy-manim.mobject.geometry.arc]
ignore_errors = True
[mypy-manim.mobject.geometry.labeled]
ignore_errors = True
[mypy-manim.mobject.geometry.line]
ignore_errors = True
[mypy-manim.mobject.geometry.polygram]
ignore_errors = True
[mypy-manim.mobject.geometry.shape_matchers]
[mypy-manim.camera.mapping_camera]
ignore_errors = True
[mypy-manim.mobject.graphing.coordinate_systems]
@ -142,21 +103,6 @@ ignore_errors = True
[mypy-manim.mobject.mobject]
ignore_errors = True
[mypy-manim.mobject.opengl.dot_cloud]
ignore_errors = True
[mypy-manim.mobject.opengl.opengl_compatibility]
ignore_errors = True
[mypy-manim.mobject.opengl.opengl_geometry]
ignore_errors = True
[mypy-manim.mobject.opengl.opengl_image_mobject]
ignore_errors = True
[mypy-manim.mobject.opengl.opengl_mobject]
ignore_errors = True
[mypy-manim.mobject.opengl.opengl_point_cloud_mobject]
ignore_errors = True
@ -178,27 +124,6 @@ ignore_errors = True
[mypy-manim.mobject.table]
ignore_errors = True
[mypy-manim.mobject.text.code_mobject]
ignore_errors = True
[mypy-manim.mobject.text.tex_mobject]
ignore_errors = True
[mypy-manim.mobject.text.text_mobject]
ignore_errors = True
[mypy-manim.mobject.three_d.polyhedra]
ignore_errors = True
[mypy-manim.mobject.three_d.three_dimensions]
ignore_errors = True
[mypy-manim.mobject.types.image_mobject]
ignore_errors = True
[mypy-manim.mobject.types.point_cloud_mobject]
ignore_errors = True
[mypy-manim.mobject.types.vectorized_mobject]
ignore_errors = True
@ -220,44 +145,9 @@ ignore_errors = True
[mypy-manim.renderer.shader_wrapper]
ignore_errors = True
[mypy-manim.renderer.vectorized_mobject_rendering]
ignore_errors = True
[mypy-manim.scene.scene]
ignore_errors = True
[mypy-manim.scene.vector_space_scene]
ignore_errors = True
[mypy-manim.scene.zoomed_scene]
ignore_errors = True
[mypy-manim.utils.caching]
ignore_errors = True
[mypy-manim.utils.color.core]
ignore_errors = True
[mypy-manim.utils.debug]
ignore_errors = True
[mypy-manim.utils.directories]
ignore_errors = True
[mypy-manim.utils.family_ops]
ignore_errors = True
[mypy-manim.utils.hashing]
ignore_errors = True
[mypy-manim.utils.progressbar]
ignore_errors = True
[mypy-manim.utils.space_ops]
ignore_errors = True
[mypy-manim.utils.testing._test_class_makers]
ignore_errors = True
[mypy-manim.utils.testing.frames_comparison]
ignore_errors = True

View file

@ -1,6 +1,6 @@
[project]
name = "manim"
version = "0.19.2"
version = "0.20.0"
description = "Animation engine for explanatory math videos."
authors = [
{name = "The Manim Community Developers", email = "contact@manim.community"},

View file

@ -13,8 +13,7 @@ def main():
sys.exit(1)
npz_file = sys.argv[1]
output_folder = pathlib.Path(sys.argv[2])
if not output_folder.exists():
output_folder.mkdir(parents=True)
output_folder.mkdir(parents=True, exist_ok=True)
data = np.load(npz_file)
if "frame_data" not in data:

View file

@ -346,7 +346,12 @@ def cli(ctx: click.Context, dry_run: bool) -> None:
)
@click.option("--head", default="main", help="Head ref for comparison (default: main)")
@click.option("--title", help="Custom changelog title (default: vX.Y.Z)")
@click.option("--update-citation", is_flag=True, help="Also update CITATION.cff")
@click.option(
"--update-citation",
"also_update_citation",
is_flag=True,
help="Also update CITATION.cff",
)
@click.pass_context
def changelog(
ctx: click.Context,
@ -354,7 +359,7 @@ def changelog(
version: str,
head: str,
title: str | None,
update_citation: bool,
also_update_citation: bool,
) -> None:
"""Generate changelog for an upcoming release.
@ -375,7 +380,7 @@ def changelog(
click.echo()
click.secho("[DRY RUN]", fg="yellow", bold=True)
click.echo(f" Would save: {CHANGELOG_DIR / f'{version}-changelog.md'}")
if update_citation:
if also_update_citation:
click.echo(f" Would update: {CITATION_FILE}")
click.echo()
click.echo("--- Generated changelog ---")
@ -385,7 +390,7 @@ def changelog(
filepath = save_changelog(version, content)
click.secho(f" ✓ Saved: {filepath}", fg="green")
if update_citation:
if also_update_citation:
citation_path = update_citation(version)
click.secho(f" ✓ Updated: {citation_path}", fg="green")

View file

@ -1,8 +1,8 @@
{"levelname": "INFO", "module": "logger_utils", "message": "Log file will be saved in <>"}
{"levelname": "INFO", "module": "tex_file_writing", "message": "Writing <> to <>"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: LaTeX Error: File `notapackage.sty' not found.\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\special{dvisvgm:raw <g id='unique000'>}\\frac{1}{0}\\special{dvisvgm:raw </g>}\n"}
{"levelname": "INFO", "module": "tex_file_writing", "message": "You do not have package notapackage.sty installed."}
{"levelname": "INFO", "module": "tex_file_writing", "message": "Install notapackage.sty it using your LaTeX package manager, or check for typos."}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: Emergency stop.\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"}
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\special{dvisvgm:raw <g id='unique000'>}\\frac{1}{0}\\special{dvisvgm:raw </g>}\n"}

View file

@ -54,7 +54,6 @@ def set_test_scene(scene_object: type[Scene], module_name: str, config):
tests_directory = Path(__file__).absolute().parent.parent
path_control_data = Path(tests_directory) / "control_data" / "graphical_units_data"
path = Path(path_control_data) / module_name
if not path.is_dir():
path.mkdir(parents=True)
path.mkdir(parents=True, exist_ok=True)
np.savez_compressed(path / str(manager.scene), frame_data=data)
logger.info(f"Test data for {str(manager.scene)} saved in {path}\n")

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from manim import Circle, FadeIn
from manim import UP, Circle, Dot, FadeIn
from manim.animation.updaters.mobject_update_utils import turn_animation_into_updater
@ -56,3 +56,14 @@ def test_turn_animation_into_updater_positive_run_time_persists():
current_updaters = mobject.get_updaters()
assert len(current_updaters) == len(original_updaters) + 1
assert updater in current_updaters
def test_always():
d = Dot()
circ = Circle()
d.always.next_to(circ, UP)
assert len(d.updaters) == 1
# we should be able to chain updaters
d2 = Dot()
d.always.next_to(d2, UP).next_to(circ, UP)
assert len(d.updaters) == 3

View file

@ -90,7 +90,7 @@ def test_Polygram_get_vertex_groups():
for vertex_groups in vertex_groups_arr:
polygram = Polygram(*vertex_groups)
poly_vertex_groups = polygram.get_vertex_groups()
for poly_group, group in zip(poly_vertex_groups, vertex_groups, strict=False):
for poly_group, group in zip(poly_vertex_groups, vertex_groups, strict=True):
np.testing.assert_array_equal(poly_group, group)
# If polygram is a Polygram of a vertex group containing the start vertex N times,

View file

@ -62,7 +62,7 @@ def test_add_labels():
expected_label_length = 6
num_line = NumberLine(x_range=[-4, 4])
num_line.add_labels(
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=False)),
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=True)),
)
actual_label_length = len(num_line.labels)
assert actual_label_length == expected_label_length, (

View file

@ -27,7 +27,7 @@ def test_bracelabel_copy(tmp_path, config):
config["text_dir"] = str(mediadir.joinpath("Text"))
config["tex_dir"] = str(mediadir.joinpath("Tex"))
for el in ["text_dir", "tex_dir"]:
Path(config[el]).mkdir(parents=True)
Path(config[el]).mkdir(parents=True, exist_ok=True)
# Before the refactoring of Mobject.copy(), the class BraceLabel was the
# only one to have a non-trivial definition of copy. Here we test that it

View file

@ -10,7 +10,7 @@ from manim import MathTex, SingleStringMathTex, Tex, TexTemplate, tempconfig
def test_MathTex(config):
MathTex("a^2 + b^2 = c^2")
assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists()
assert Path(config.media_dir, "Tex", "05bb0a41ed575f00.svg").exists()
def test_SingleStringMathTex(config):
@ -29,7 +29,7 @@ def test_double_braces_testing(text_input, length_sub):
def test_tex(config):
Tex("The horse does not eat cucumber salad.")
assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists()
assert Path(config.media_dir, "Tex", "5384b41741a246bd.svg").exists()
def test_tex_temp_directory(tmpdir, monkeypatch):
@ -38,16 +38,16 @@ def test_tex_temp_directory(tmpdir, monkeypatch):
# tempconfig to change media directory to temporary directory by default
# we partially, revert that change here.
monkeypatch.chdir(tmpdir)
Path(tmpdir, "media").mkdir()
Path(tmpdir, "media").mkdir(exist_ok=True)
with tempconfig({"media_dir": "media"}):
Tex("The horse does not eat cucumber salad.")
assert Path("media", "Tex").exists()
assert Path("media", "Tex", "c3945e23e546c95a.svg").exists()
assert Path("media", "Tex", "5384b41741a246bd.svg").exists()
def test_percent_char_rendering(config):
Tex(r"\%")
assert Path(config.media_dir, "Tex", "4a583af4d19a3adf.tex").exists()
assert Path(config.media_dir, "Tex", "32509dd0ea993961.tex").exists()
def test_tex_whitespace_arg():
@ -108,7 +108,7 @@ def test_multi_part_tex_with_empty_parts():
for one_part_glyph, multi_part_glyph in zip(
one_part_fomula.family_members_with_points(),
multi_part_formula.family_members_with_points(),
strict=False,
strict=True,
):
np.testing.assert_allclose(one_part_glyph.points, multi_part_glyph.points)
@ -215,14 +215,14 @@ def test_tempconfig_resetting_tex_template(config):
def test_tex_garbage_collection(tmpdir, monkeypatch, config):
monkeypatch.chdir(tmpdir)
Path(tmpdir, "media").mkdir()
Path(tmpdir, "media").mkdir(exist_ok=True)
config.media_dir = "media"
tex_without_log = Tex("Hello World!") # d771330b76d29ffb.tex
assert Path("media", "Tex", "d771330b76d29ffb.tex").exists()
assert not Path("media", "Tex", "d771330b76d29ffb.log").exists()
tex_without_log = Tex("Hello World!") # 058a4e242c57db6d.tex
assert Path("media", "Tex", "058a4e242c57db6d.tex").exists()
assert not Path("media", "Tex", "058a4e242c57db6d.log").exists()
config.no_latex_cleanup = True
tex_with_log = Tex("Hello World, again!") # da27670a37b08799.tex
assert Path("media", "Tex", "da27670a37b08799.log").exists()
tex_with_log = Tex("Hello World, again!") # 45b4e7819cc20cb1.tex
assert Path("media", "Tex", "45b4e7819cc20cb1.log").exists()

View file

@ -408,13 +408,7 @@ def test_vdict_init():
# Test VDict made from a python dict
VDict({"a": VMobject(), "b": VMobject(), "c": VMobject()})
# Test VDict made using zip
VDict(
zip(
["a", "b", "c"],
[VMobject(), VMobject(), VMobject()],
strict=False,
)
)
VDict(zip(["a", "b", "c"], [VMobject(), VMobject(), VMobject()], strict=True))
# If the value is of type Mobject, must raise a TypeError
with pytest.raises(TypeError):
VDict({"a": Mobject()})

View file

@ -32,7 +32,7 @@ def test_custom_coordinates(scene: Scene) -> None:
ax = Axes(x_range=[0, 10])
ax.add_coordinates(
dict(zip(list(range(1, 10)), [Tex("str") for _ in range(1, 10)], strict=False)),
dict(zip(list(range(1, 10)), [Tex("str") for _ in range(1, 10)], strict=True)),
)
scene.add(ax)
@ -226,7 +226,7 @@ def test_get_area(scene: Scene) -> None:
area2 = ax.get_area(
curve1,
x_range=(-4.5, -2),
color=(RED, YELLOW),
color=(RED, PURE_YELLOW),
opacity=0.2,
bounded_graph=curve2,
)
@ -266,7 +266,7 @@ def test_get_riemann_rectangles(scene: Scene, use_vectorized: bool) -> None:
quadratic,
x_range=[-1.5, 1.5],
dx=0.15,
color=YELLOW,
color=PURE_YELLOW,
)
bounding_line = ax.plot(lambda x: 1.5 * x, color=BLUE_B, x_range=[3.3, 6])

View file

@ -28,7 +28,7 @@ def test_line_graph(scene):
first_line = plane.plot_line_graph(
x_values=[-3, 1],
y_values=[-2, 2],
line_color=YELLOW,
line_color=PURE_YELLOW,
)
second_line = plane.plot_line_graph(
x_values=[0, 2, 2, 4],
@ -75,7 +75,7 @@ def test_plot_surface_colorscale(scene):
param_trig,
u_range=(-3, 3),
v_range=(-3, 3),
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED],
)
scene.add(axes, trig_plane)
@ -156,7 +156,7 @@ def test_gradient_line_graph_x_axis(scene):
curve = axes.plot(
lambda x: 0.1 * x**3,
x_range=(-3, 3, 0.001),
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED],
colorscale_axis=0,
)
@ -172,7 +172,7 @@ def test_gradient_line_graph_y_axis(scene):
curve = axes.plot(
lambda x: 0.1 * x**3,
x_range=(-3, 3, 0.001),
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED],
colorscale_axis=1,
)

View file

@ -268,21 +268,16 @@ def test_ImageInterpolation(scene):
img = ImageMobject(
np.uint8([[63, 0, 0, 0], [0, 127, 0, 0], [0, 0, 191, 0], [0, 0, 0, 255]]),
)
img.height = 2
img1 = img.copy()
img2 = img.copy()
img3 = img.copy()
img4 = img.copy()
img5 = img.copy()
img.height = 3
img1.set_resampling_algorithm(RESAMPLING_ALGORITHMS["nearest"])
img2.set_resampling_algorithm(RESAMPLING_ALGORITHMS["lanczos"])
img3.set_resampling_algorithm(RESAMPLING_ALGORITHMS["linear"])
img4.set_resampling_algorithm(RESAMPLING_ALGORITHMS["cubic"])
img5.set_resampling_algorithm(RESAMPLING_ALGORITHMS["box"])
algorithm_texts = ["nearest", "linear", "cubic"]
for i, algorithm_text in enumerate(algorithm_texts):
algorithm = RESAMPLING_ALGORITHMS[algorithm_text]
img_copy = img.copy().set_resampling_algorithm(algorithm)
position = img.height * (i - (len(algorithm_texts) - 1) / 2) * RIGHT
img_copy.move_to(position)
scene.add(img_copy)
scene.add(img1, img2, img3, img4, img5)
[s.shift(4 * LEFT + pos * 2 * RIGHT) for pos, s in enumerate(scene.mobjects)]
scene.wait()

View file

@ -24,7 +24,7 @@ def test_become(scene):
.set_opacity(0.25)
.set_color(GREEN)
)
s3 = s.copy().become(d, stretch=True).set_opacity(0.25).set_color(YELLOW)
s3 = s.copy().become(d, stretch=True).set_opacity(0.25).set_color(PURE_YELLOW)
scene.add(s, d, s1, s2, s3)
@ -37,7 +37,7 @@ def test_become_no_color_linking(scene):
scene.add(b)
b.become(a)
b.shift(1 * RIGHT)
b.set_stroke(YELLOW, opacity=1)
b.set_stroke(PURE_YELLOW, opacity=1)
@frames_comparison
@ -59,7 +59,7 @@ def test_vmobject_joint_types(scene):
]
)
lines = VGroup(*[angled_line.copy() for _ in range(len(LineJointType))])
for line, joint_type in zip(lines, LineJointType, strict=False):
for line, joint_type in zip(lines, LineJointType, strict=True):
line.joint_type = joint_type
lines.arrange(RIGHT, buff=1)

View file

@ -8,11 +8,11 @@ __module_test__ = "modifier_methods"
@frames_comparison
def test_Gradient(scene):
c = Circle(fill_opacity=1).set_color(color=[YELLOW, GREEN])
c = Circle(fill_opacity=1).set_color(color=[PURE_YELLOW, GREEN])
scene.add(c)
@frames_comparison
def test_GradientRotation(scene):
c = Circle(fill_opacity=1).set_color(color=[YELLOW, GREEN]).rotate(PI)
c = Circle(fill_opacity=1).set_color(color=[PURE_YELLOW, GREEN]).rotate(PI)
scene.add(c)

View file

@ -3,7 +3,7 @@ import pytest
from manim.constants import LEFT
from manim.mobject.graphing.probability import BarChart
from manim.mobject.text.tex_mobject import MathTex
from manim.utils.color import BLUE, GREEN, RED, WHITE, YELLOW
from manim.utils.color import BLUE, GREEN, PURE_YELLOW, RED, WHITE
from manim.utils.testing.frames_comparison import frames_comparison
__module_test__ = "probability"
@ -72,13 +72,13 @@ def test_advanced_customization(scene):
chart = BarChart(values=[10, 40, 10, 20], bar_names=["one", "two", "three", "four"])
c_x_lbls = chart.x_axis.labels
c_x_lbls.set_color_by_gradient(GREEN, RED, YELLOW)
c_x_lbls.set_color_by_gradient(GREEN, RED, PURE_YELLOW)
c_y_nums = chart.y_axis.numbers
c_y_nums.set_color_by_gradient(BLUE, WHITE).shift(LEFT)
c_y_axis = chart.y_axis
c_y_axis.ticks.set_color(YELLOW)
c_y_axis.ticks.set_color(PURE_YELLOW)
c_bar_lbls = chart.get_bar_labels()

View file

@ -145,7 +145,7 @@ def test_SurfaceColorscale(scene: Scene) -> None:
u_range=[-3, 3],
)
trig_plane.set_fill_by_value(
axes=axes, colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED]
axes=axes, colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED]
)
scene.add(axes, trig_plane)
@ -170,7 +170,7 @@ def test_Y_Direction(scene: Scene) -> None:
)
surface_plane.set_style(fill_opacity=1)
surface_plane.set_fill_by_value(
axes=axes, colorscale=[(RED, -0.4), (YELLOW, 0), (GREEN, 0.4)], axis=1
axes=axes, colorscale=[(RED, -0.4), (PURE_YELLOW, 0), (GREEN, 0.4)], axis=1
)
scene.add(axes, surface_plane)

View file

@ -159,7 +159,7 @@ def test_AnimationBuilder(scene):
@frames_comparison(last_frame=False)
def test_ReplacementTransform(scene):
yellow = Square(fill_opacity=1.0, fill_color=YELLOW)
yellow = Square(fill_opacity=1.0, fill_color=PURE_YELLOW)
yellow.move_to([0, 0.75, 0])
green = Square(fill_opacity=1.0, fill_color=GREEN)

View file

@ -105,7 +105,7 @@ def create_plugin(tmp_path, python_version, random_string):
def _create_plugin(entry_point, class_name, function_name, all_dec=""):
entry_point = entry_point.format(plugin_name=plugin_name)
module_dir = plugin_dir / plugin_name
module_dir.mkdir(parents=True)
module_dir.mkdir(parents=True, exist_ok=True)
(module_dir / "__init__.py").write_text(
plugin_init_template.format(
class_name=class_name,

View file

@ -28,7 +28,9 @@ def _check_logs(reference_logfile_path: Path, generated_logfile_path: Path) -> N
msg_assert += f"\nPath of reference log: {reference_logfile}\nPath of generated logs: {generated_logfile}"
pytest.fail(msg_assert)
for index, ref, gen in zip(itertools.count(), reference_logs, generated_logs):
for index, ref, gen in zip(
itertools.count(), reference_logs, generated_logs, strict=False
):
# As they are string, we only need to check if they are equal. If they are not, we then compute a more precise difference, to debug.
if ref == gen:
continue

View file

@ -59,7 +59,7 @@ def check_video_data(path_control_data: Path, path_video_gen: Path) -> None:
f"expected {len(sec_index_exp)} sections ({', '.join([el['name'] for el in sec_index_exp])}), but {len(sec_index_gen)} ({', '.join([el['name'] for el in sec_index_gen])}) got generated (in '{path_sec_index_gen}')"
)
# check individual sections
for sec_gen, sec_exp in zip(sec_index_gen, sec_index_exp, strict=False):
for sec_gen, sec_exp in zip(sec_index_gen, sec_index_exp, strict=True):
assert_shallow_dict_compare(
sec_gen,
sec_exp,

170
uv.lock generated
View file

@ -1396,7 +1396,7 @@ wheels = [
[[package]]
name = "manim"
version = "0.19.2"
version = "0.20.0"
source = { editable = "." }
dependencies = [
{ name = "audioop-lts", marker = "python_full_version >= '3.13'" },
@ -1873,7 +1873,7 @@ wheels = [
[[package]]
name = "nbconvert"
version = "7.16.6"
version = "7.17.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
@ -1891,9 +1891,9 @@ dependencies = [
{ name = "pygments" },
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715, upload-time = "2025-01-28T09:29:14.724Z" }
sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525, upload-time = "2025-01-28T09:29:12.551Z" },
{ url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" },
]
[[package]]
@ -2095,89 +2095,89 @@ wheels = [
[[package]]
name = "pillow"
version = "12.1.0"
version = "12.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" },
{ url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" },
{ url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" },
{ url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" },
{ url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" },
{ url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" },
{ url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" },
{ url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" },
{ url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" },
{ url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" },
{ url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" },
{ url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" },
{ url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" },
{ url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" },
{ url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" },
{ url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" },
{ url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" },
{ url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" },
{ url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" },
{ url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" },
{ url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" },
{ url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" },
{ url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" },
{ url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" },
{ url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" },
{ url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" },
{ url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" },
{ url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" },
{ url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" },
{ url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" },
{ url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" },
{ url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" },
{ url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" },
{ url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" },
{ url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" },
{ url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" },
{ url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" },
{ url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" },
{ url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" },
{ url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" },
{ url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" },
{ url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" },
{ url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" },
{ url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" },
{ url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" },
{ url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" },
{ url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" },
{ url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" },
{ url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" },
{ url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" },
{ url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" },
{ url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" },
{ url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" },
{ url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" },
{ url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" },
{ url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" },
{ url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" },
{ url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" },
{ url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" },
{ url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" },
{ url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" },
{ url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" },
{ url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" },
{ url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" },
{ url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" },
{ url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" },
{ url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" },
{ url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" },
{ url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" },
{ url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" },
{ url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" },
{ url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" },
{ url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" },
{ url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" },
{ url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" },
{ url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" },
{ url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" },
{ url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" },
{ url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" },
{ url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" },
{ url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" },
{ url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" },
{ url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
{ url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
{ url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
{ url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
{ url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
{ url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
{ url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
{ url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
{ url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
{ url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
{ url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
{ url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
{ url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
{ url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
{ url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
{ url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
{ url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
{ url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
{ url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
{ url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
{ url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
{ url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
{ url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
{ url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
{ url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
{ url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
{ url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
{ url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
{ url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
{ url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
{ url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
{ url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
{ url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
{ url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
{ url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
{ url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
{ url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
{ url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
{ url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
{ url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
{ url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
{ url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" },
{ url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" },
{ url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" },
{ url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" },
{ url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" },
{ url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" },
{ url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
]
[[package]]