manim-simplex 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/PKG-INFO +2 -1
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/pyproject.toml +2 -1
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/animations.py +6 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/mobjects/__init__.py +13 -1
- manim_simplex-0.2.2/src/simplex/mobjects/paper.py +456 -0
- manim_simplex-0.2.2/tests/mobjects/test_paper.py +136 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/uv.lock +19 -1
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/.gitignore +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/.pre-commit-config.yaml +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/.python-version +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/CHANGELOG.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/LICENSE +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/README.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/examples/README.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/examples/glyph_map_demo.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/examples/hello_slide.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/examples/manim.cfg +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/examples/outline_slide.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/examples/theme_demo.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/ruff.toml +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/README.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/__init__.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/code.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/debug.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/defaults.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/dynamics.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/geometry.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/ghost_fade.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/glyph_map.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/region.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/scaling.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/engine/text.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/manifest.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/mobjects/README.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/mobjects/array.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/mobjects/graph.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/mobjects/outline.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/plugin.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/py.typed +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/section.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/slides/README.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/slides/__init__.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/slides/base.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/slides/chrome.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/slides/outline.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/theme/README.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/theme/__init__.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/theme/context.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/theme/presets.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/theme/pygments_style.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/theme/tokens.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/src/simplex/theme/web_css.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/README.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/__init__.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/README.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/__init__.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/test_animations.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/test_code.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/test_debug.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/test_dynamics.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/test_geometry.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/test_ghost_fade.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/test_glyph_map.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/test_region.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/test_scaling.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/engine/test_text.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/mobjects/README.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/mobjects/__init__.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/mobjects/test_graph.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/mobjects/test_outline.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/slides/README.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/slides/__init__.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/slides/test_base.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/slides/test_chrome.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/slides/test_outline.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/test_manifest.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/test_section.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/theme/README.md +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/theme/__init__.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/theme/test_tokens.py +0 -0
- {manim_simplex-0.2.1 → manim_simplex-0.2.2}/tests/theme/test_web_css.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: manim-simplex
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Manim plugin: theme tokens, mobjects, slide hierarchy, deck manifest schema.
|
|
5
5
|
Project-URL: Changelog, https://github.com/shlomi-perles/manim-simplex/blob/main/CHANGELOG.md
|
|
6
6
|
Project-URL: Homepage, https://github.com/shlomi-perles/manim-simplex
|
|
@@ -28,6 +28,7 @@ Requires-Dist: manim-slides>=5.1.7
|
|
|
28
28
|
Requires-Dist: manim>=0.20.1
|
|
29
29
|
Requires-Dist: pydantic>=2.7
|
|
30
30
|
Requires-Dist: pygments>=2.18
|
|
31
|
+
Requires-Dist: pymupdf>=1.27.2.3
|
|
31
32
|
Description-Content-Type: text/markdown
|
|
32
33
|
|
|
33
34
|
# manim-simplex
|
|
@@ -33,6 +33,7 @@ dependencies = [
|
|
|
33
33
|
"manim-slides>=5.1.7",
|
|
34
34
|
"pydantic>=2.7",
|
|
35
35
|
"pygments>=2.18",
|
|
36
|
+
"pymupdf>=1.27.2.3",
|
|
36
37
|
]
|
|
37
38
|
description = "Manim plugin: theme tokens, mobjects, slide hierarchy, deck manifest schema."
|
|
38
39
|
keywords = [
|
|
@@ -50,7 +51,7 @@ license-files = ["LICENSE"]
|
|
|
50
51
|
name = "manim-simplex"
|
|
51
52
|
readme = "README.md"
|
|
52
53
|
requires-python = ">=3.13"
|
|
53
|
-
version = "0.2.
|
|
54
|
+
version = "0.2.2"
|
|
54
55
|
|
|
55
56
|
[project.entry-points."manim.plugins"]
|
|
56
57
|
simplex = "simplex.plugin:activate"
|
|
@@ -66,10 +66,16 @@ class _DefaultRegistry:
|
|
|
66
66
|
VMobject,
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
+
from simplex.mobjects.paper import DismissPaper, Paper
|
|
70
|
+
|
|
69
71
|
def fade_with_drift(m: Any, **kw: Any) -> Any:
|
|
70
72
|
return FadeOut(m, shift=0.1 * DOWN, **kw)
|
|
71
73
|
|
|
74
|
+
def paper_exit(m: Any, **kw: Any) -> Any:
|
|
75
|
+
return DismissPaper(m, **kw)
|
|
76
|
+
|
|
72
77
|
return {
|
|
78
|
+
Paper: paper_exit,
|
|
73
79
|
Tex: Unwrite,
|
|
74
80
|
MathTex: Unwrite,
|
|
75
81
|
Text: Unwrite,
|
|
@@ -9,5 +9,17 @@ slide system.
|
|
|
9
9
|
from simplex.mobjects.array import ArrayEntry, ArrayMob, ArrayPointer
|
|
10
10
|
from simplex.mobjects.graph import Edge, Node
|
|
11
11
|
from simplex.mobjects.outline import OutlineProgressBar
|
|
12
|
+
from simplex.mobjects.paper import DismissPaper, Paper, PickPage, ShowPaper
|
|
12
13
|
|
|
13
|
-
__all__ = [
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ArrayEntry",
|
|
16
|
+
"ArrayMob",
|
|
17
|
+
"ArrayPointer",
|
|
18
|
+
"DismissPaper",
|
|
19
|
+
"Edge",
|
|
20
|
+
"Node",
|
|
21
|
+
"OutlineProgressBar",
|
|
22
|
+
"Paper",
|
|
23
|
+
"PickPage",
|
|
24
|
+
"ShowPaper",
|
|
25
|
+
]
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"""Paper mobject -- render academic papers (ArXiv / local PDF / BibTeX) as stacked page images.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- ``Paper``: a ``Group`` of ``ImageMobject`` pages with configurable shadow and stacking.
|
|
5
|
+
- ``ShowPaper``: intro animation that builds the stacked view.
|
|
6
|
+
- ``DismissPaper``: exit animation — delegates to ``ShowPaper`` with reversed fade direction.
|
|
7
|
+
- ``PickPage``: pull-from-stack animation for a given page index.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
import urllib.request
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pymupdf
|
|
20
|
+
from manim import (
|
|
21
|
+
DOWN,
|
|
22
|
+
LEFT,
|
|
23
|
+
RIGHT,
|
|
24
|
+
UP,
|
|
25
|
+
WHITE,
|
|
26
|
+
Animation,
|
|
27
|
+
AnimationGroup,
|
|
28
|
+
FadeIn,
|
|
29
|
+
FadeOut,
|
|
30
|
+
Group,
|
|
31
|
+
ImageMobject,
|
|
32
|
+
Rectangle,
|
|
33
|
+
RoundedRectangle,
|
|
34
|
+
config,
|
|
35
|
+
smooth,
|
|
36
|
+
)
|
|
37
|
+
from manim.utils.tex_file_writing import tex_hash
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger("simplex.paper")
|
|
40
|
+
|
|
41
|
+
_DEFAULT_DPI = 150
|
|
42
|
+
_DEFAULT_PAGES = 3
|
|
43
|
+
_DEFAULT_TIMEOUT = 30
|
|
44
|
+
_SHADOW_OPACITY = 0.35
|
|
45
|
+
_SHADOW_COLOR = "#000000"
|
|
46
|
+
_SHADOW_OFFSET_FACTOR = 0.06
|
|
47
|
+
_STACK_OFFSET_FACTOR = 0.08
|
|
48
|
+
_PAGE_HEIGHT = 5.5
|
|
49
|
+
_BORDER_COLOR = WHITE
|
|
50
|
+
_BORDER_STROKE_WIDTH = 1.5
|
|
51
|
+
|
|
52
|
+
_ARXIV_ABS_RE = re.compile(r"arxiv\.org/abs/(.+?)(?:\?|$)")
|
|
53
|
+
_ARXIV_PDF_RE = re.compile(r"arxiv\.org/pdf/(.+?)(?:\.pdf)?(?:\?|$)")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _paper_dir() -> Path:
|
|
57
|
+
"""Return (and create) the paper cache directory inside Manim's media tree."""
|
|
58
|
+
d = Path(config.media_dir) / "papers"
|
|
59
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
return d
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _url_to_pdf_url(url: str) -> str:
|
|
64
|
+
"""Normalize an ArXiv URL to a direct PDF download link."""
|
|
65
|
+
if m := _ARXIV_ABS_RE.search(url):
|
|
66
|
+
return f"https://arxiv.org/pdf/{m.group(1)}.pdf"
|
|
67
|
+
if _ARXIV_PDF_RE.search(url):
|
|
68
|
+
return url if url.endswith(".pdf") else url + ".pdf"
|
|
69
|
+
return url
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _download_pdf(url: str, *, timeout: int = _DEFAULT_TIMEOUT) -> Path:
|
|
73
|
+
"""Download a PDF from *url*, caching on disk. Returns local path."""
|
|
74
|
+
key = tex_hash(url)
|
|
75
|
+
cached = _paper_dir() / f"{key}.pdf"
|
|
76
|
+
if cached.exists():
|
|
77
|
+
return cached
|
|
78
|
+
pdf_url = _url_to_pdf_url(url)
|
|
79
|
+
if not pdf_url.startswith(("https://", "http://")):
|
|
80
|
+
raise ValueError(f"Refusing to open non-HTTP URL: {pdf_url}")
|
|
81
|
+
logger.info("Downloading %s → %s", pdf_url, cached)
|
|
82
|
+
req = urllib.request.Request(pdf_url, headers={"User-Agent": "manim-simplex/0.2"}) # noqa: S310
|
|
83
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
|
|
84
|
+
cached.write_bytes(resp.read())
|
|
85
|
+
return cached
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _resolve_bibtex_source(bib_path: Path, cite_key: str) -> str:
|
|
89
|
+
"""Extract an ArXiv URL or ``eprint`` field from a BibTeX entry."""
|
|
90
|
+
text = bib_path.read_text()
|
|
91
|
+
pattern = re.compile(
|
|
92
|
+
rf"@\w+\{{\s*{re.escape(cite_key)}\s*,(.*?)\n\s*\}}",
|
|
93
|
+
re.DOTALL,
|
|
94
|
+
)
|
|
95
|
+
match = pattern.search(text)
|
|
96
|
+
if not match:
|
|
97
|
+
raise ValueError(f"Cite key '{cite_key}' not found in {bib_path}")
|
|
98
|
+
body = match.group(1)
|
|
99
|
+
if ep := re.search(r"eprint\s*=\s*\{?([0-9.]+)\}?", body):
|
|
100
|
+
return f"https://arxiv.org/abs/{ep.group(1)}"
|
|
101
|
+
if url_match := re.search(r"url\s*=\s*\{(.+?)\}", body):
|
|
102
|
+
return url_match.group(1)
|
|
103
|
+
raise ValueError(f"No ArXiv eprint or URL found for '{cite_key}' in {bib_path}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _render_pages(
|
|
107
|
+
pdf_path: Path,
|
|
108
|
+
*,
|
|
109
|
+
pages: int = _DEFAULT_PAGES,
|
|
110
|
+
dpi: int = _DEFAULT_DPI,
|
|
111
|
+
) -> list[Path]:
|
|
112
|
+
"""Render the first *pages* pages of a PDF to cached PNGs."""
|
|
113
|
+
key = tex_hash(str(pdf_path))
|
|
114
|
+
cache = _paper_dir() / key
|
|
115
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
|
|
117
|
+
rendered: list[Path] = []
|
|
118
|
+
doc = pymupdf.open(pdf_path)
|
|
119
|
+
n = min(pages, len(doc))
|
|
120
|
+
for i in range(n):
|
|
121
|
+
out = cache / f"page_{i}_dpi{dpi}.png"
|
|
122
|
+
if not out.exists():
|
|
123
|
+
page = doc[i]
|
|
124
|
+
zoom = dpi / 72.0
|
|
125
|
+
mat = pymupdf.Matrix(zoom, zoom)
|
|
126
|
+
pix = page.get_pixmap(matrix=mat, alpha=False)
|
|
127
|
+
pix.save(out)
|
|
128
|
+
logger.info("Rendered %s page %d → %s", pdf_path.name, i, out)
|
|
129
|
+
rendered.append(out)
|
|
130
|
+
doc.close()
|
|
131
|
+
return rendered
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# Direction parsing (shared by Paper and animation classes)
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
_DIRECTION_MAP: dict[str, np.ndarray] = {
|
|
139
|
+
"dl": DOWN + LEFT,
|
|
140
|
+
"dr": DOWN + RIGHT,
|
|
141
|
+
"ul": UP + LEFT,
|
|
142
|
+
"ur": UP + RIGHT,
|
|
143
|
+
"left": LEFT,
|
|
144
|
+
"right": RIGHT,
|
|
145
|
+
"up": UP,
|
|
146
|
+
"down": DOWN,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _parse_direction(direction: str | np.ndarray) -> np.ndarray:
|
|
151
|
+
if isinstance(direction, str):
|
|
152
|
+
key = direction.lower().strip()
|
|
153
|
+
if key in _DIRECTION_MAP:
|
|
154
|
+
return _DIRECTION_MAP[key]
|
|
155
|
+
raise ValueError(f"Unknown direction '{direction}'.")
|
|
156
|
+
return np.asarray(direction, dtype=float)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Paper mobject
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class Paper(Group):
|
|
165
|
+
"""A stack of rendered PDF pages displayed as ``ImageMobject`` instances.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
source
|
|
170
|
+
One of: ArXiv URL, local PDF path, or ``(bib_path, cite_key)`` tuple.
|
|
171
|
+
pages
|
|
172
|
+
Number of pages to render (from the start of the document).
|
|
173
|
+
dpi
|
|
174
|
+
Resolution for PDF-to-image conversion.
|
|
175
|
+
page_height
|
|
176
|
+
Target height of each page in Manim units.
|
|
177
|
+
shadow
|
|
178
|
+
Whether to render a drop shadow behind pages.
|
|
179
|
+
shadow_direction
|
|
180
|
+
Direction the shadow falls (offset direction from page center).
|
|
181
|
+
shadow_opacity
|
|
182
|
+
Fill opacity of the shadow rectangles.
|
|
183
|
+
border
|
|
184
|
+
Whether to draw a thin border around each page.
|
|
185
|
+
border_color
|
|
186
|
+
Stroke color for the page border.
|
|
187
|
+
border_stroke_width
|
|
188
|
+
Stroke width for the page border.
|
|
189
|
+
stack_direction
|
|
190
|
+
Direction pages stack towards (the offset axis for peeking edges).
|
|
191
|
+
stack_offset
|
|
192
|
+
Distance between consecutive pages in the stack (Manim units).
|
|
193
|
+
timeout
|
|
194
|
+
Network timeout in seconds for downloading.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
source: str | Path | tuple[Path | str, str],
|
|
200
|
+
*,
|
|
201
|
+
pages: int = _DEFAULT_PAGES,
|
|
202
|
+
dpi: int = _DEFAULT_DPI,
|
|
203
|
+
page_height: float = _PAGE_HEIGHT,
|
|
204
|
+
shadow: bool = True,
|
|
205
|
+
shadow_direction: str | np.ndarray = "DL",
|
|
206
|
+
shadow_opacity: float = _SHADOW_OPACITY,
|
|
207
|
+
border: bool = True,
|
|
208
|
+
border_color: str = _BORDER_COLOR,
|
|
209
|
+
border_stroke_width: float = _BORDER_STROKE_WIDTH,
|
|
210
|
+
stack_direction: str | np.ndarray = "DL",
|
|
211
|
+
stack_offset: float | None = None,
|
|
212
|
+
timeout: int = _DEFAULT_TIMEOUT,
|
|
213
|
+
**kwargs: Any,
|
|
214
|
+
) -> None:
|
|
215
|
+
self._shadow_enabled = shadow
|
|
216
|
+
|
|
217
|
+
pdf_path = self._resolve_source(source, timeout=timeout)
|
|
218
|
+
image_paths = _render_pages(pdf_path, pages=pages, dpi=dpi)
|
|
219
|
+
|
|
220
|
+
shadow_dir = _parse_direction(shadow_direction)
|
|
221
|
+
self._stack_dir = _parse_direction(stack_direction)
|
|
222
|
+
self._stack_offset = (
|
|
223
|
+
stack_offset if stack_offset is not None else page_height * _STACK_OFFSET_FACTOR
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
page_groups: list[Group] = []
|
|
227
|
+
for img_path in image_paths:
|
|
228
|
+
img = ImageMobject(str(img_path))
|
|
229
|
+
img.height = page_height
|
|
230
|
+
|
|
231
|
+
parts: list[Any] = []
|
|
232
|
+
|
|
233
|
+
if shadow:
|
|
234
|
+
shadow_rect = RoundedRectangle(
|
|
235
|
+
width=img.width,
|
|
236
|
+
height=img.height,
|
|
237
|
+
corner_radius=0.04,
|
|
238
|
+
fill_color=_SHADOW_COLOR,
|
|
239
|
+
fill_opacity=shadow_opacity,
|
|
240
|
+
stroke_width=0,
|
|
241
|
+
)
|
|
242
|
+
shadow_offset = shadow_dir * page_height * _SHADOW_OFFSET_FACTOR
|
|
243
|
+
shadow_rect.move_to(img.get_center() + shadow_offset)
|
|
244
|
+
parts.append(shadow_rect)
|
|
245
|
+
|
|
246
|
+
parts.append(img)
|
|
247
|
+
|
|
248
|
+
if border:
|
|
249
|
+
border_rect = Rectangle(
|
|
250
|
+
width=img.width,
|
|
251
|
+
height=img.height,
|
|
252
|
+
stroke_color=border_color,
|
|
253
|
+
stroke_width=border_stroke_width,
|
|
254
|
+
stroke_opacity=0.6,
|
|
255
|
+
fill_opacity=0,
|
|
256
|
+
)
|
|
257
|
+
border_rect.move_to(img.get_center())
|
|
258
|
+
parts.append(border_rect)
|
|
259
|
+
|
|
260
|
+
page_groups.append(Group(*parts))
|
|
261
|
+
|
|
262
|
+
# _page_groups[0] = top/front page (first PDF page), drawn last.
|
|
263
|
+
# Submobjects stored back-to-front for correct z-order.
|
|
264
|
+
self._page_groups = page_groups
|
|
265
|
+
super().__init__(*reversed(page_groups), **kwargs)
|
|
266
|
+
self._arrange_stack()
|
|
267
|
+
|
|
268
|
+
# -- source resolution ---------------------------------------------------
|
|
269
|
+
|
|
270
|
+
def _resolve_source(self, source: str | Path | tuple[Path | str, str], *, timeout: int) -> Path:
|
|
271
|
+
if isinstance(source, tuple):
|
|
272
|
+
bib_path, cite_key = source
|
|
273
|
+
url = _resolve_bibtex_source(Path(bib_path), cite_key)
|
|
274
|
+
return _download_pdf(url, timeout=timeout)
|
|
275
|
+
source_str = str(source)
|
|
276
|
+
if source_str.startswith(("http://", "https://")):
|
|
277
|
+
return _download_pdf(source_str, timeout=timeout)
|
|
278
|
+
path = Path(source_str)
|
|
279
|
+
if not path.exists():
|
|
280
|
+
raise FileNotFoundError(f"PDF not found: {path}")
|
|
281
|
+
return path
|
|
282
|
+
|
|
283
|
+
# -- layout --------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
def _arrange_stack(self) -> None:
|
|
286
|
+
"""Position pages: page 0 at origin (top), others offset behind."""
|
|
287
|
+
for i, pg in enumerate(self._page_groups):
|
|
288
|
+
pg.move_to(self._stack_dir * self._stack_offset * i)
|
|
289
|
+
|
|
290
|
+
# -- public API ----------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def page_groups(self) -> list[Group]:
|
|
294
|
+
return list(self._page_groups)
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def page_count(self) -> int:
|
|
298
|
+
return len(self._page_groups)
|
|
299
|
+
|
|
300
|
+
def get_page(self, index: int) -> Group:
|
|
301
|
+
return self._page_groups[index]
|
|
302
|
+
|
|
303
|
+
def get_top_page(self) -> Group:
|
|
304
|
+
return self._page_groups[0]
|
|
305
|
+
|
|
306
|
+
def reorder_page_to_top(self, index: int) -> None:
|
|
307
|
+
"""Move page at *index* to position 0 (front of stack, drawn last)."""
|
|
308
|
+
page = self._page_groups.pop(index)
|
|
309
|
+
self._page_groups.insert(0, page)
|
|
310
|
+
self.submobjects = list(reversed(self._page_groups))
|
|
311
|
+
self._arrange_stack()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
# Animations
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class ShowPaper(AnimationGroup):
|
|
320
|
+
"""Intro animation: pages cascade in with a lagged stagger.
|
|
321
|
+
|
|
322
|
+
Back pages appear first, then the front page lands on top — giving a
|
|
323
|
+
natural "dealing cards" effect.
|
|
324
|
+
|
|
325
|
+
When *dismiss* is ``True`` the animation flips to ``FadeOut`` and the
|
|
326
|
+
cascade order reverses (front page exits first), so ``DismissPaper``
|
|
327
|
+
can delegate here without duplicating the logic.
|
|
328
|
+
|
|
329
|
+
Parameters
|
|
330
|
+
----------
|
|
331
|
+
paper
|
|
332
|
+
The Paper mobject to animate.
|
|
333
|
+
direction
|
|
334
|
+
Direction from which pages slide in (intro) or out (dismiss).
|
|
335
|
+
lag_ratio
|
|
336
|
+
Stagger between successive page animations.
|
|
337
|
+
dismiss
|
|
338
|
+
If ``True``, use ``FadeOut`` (exit) instead of ``FadeIn`` (intro).
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
def __init__(
|
|
342
|
+
self,
|
|
343
|
+
paper: Paper,
|
|
344
|
+
*,
|
|
345
|
+
direction: str | np.ndarray = "DOWN",
|
|
346
|
+
lag_ratio: float = 0.3,
|
|
347
|
+
dismiss: bool = False,
|
|
348
|
+
**kwargs: Any,
|
|
349
|
+
) -> None:
|
|
350
|
+
shift_dir = _parse_direction(direction)
|
|
351
|
+
shift_vec = shift_dir * 2.0
|
|
352
|
+
anim_cls = FadeOut if dismiss else FadeIn
|
|
353
|
+
|
|
354
|
+
# Intro: back-to-front (last page first, top page last).
|
|
355
|
+
# Dismiss: front-to-back (top page first, last page last).
|
|
356
|
+
ordering = paper.page_groups if dismiss else list(reversed(paper.page_groups))
|
|
357
|
+
|
|
358
|
+
anims = [anim_cls(pg, shift=shift_vec) for pg in ordering]
|
|
359
|
+
kwargs.setdefault("run_time", 1.5)
|
|
360
|
+
super().__init__(*anims, lag_ratio=lag_ratio, **kwargs)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class DismissPaper(ShowPaper):
|
|
364
|
+
"""Exit animation — syntactic sugar for ``ShowPaper(..., dismiss=True)``."""
|
|
365
|
+
|
|
366
|
+
def __init__(
|
|
367
|
+
self,
|
|
368
|
+
paper: Paper,
|
|
369
|
+
*,
|
|
370
|
+
direction: str | np.ndarray = "DOWN",
|
|
371
|
+
lag_ratio: float = 0.3,
|
|
372
|
+
**kwargs: Any,
|
|
373
|
+
) -> None:
|
|
374
|
+
super().__init__(paper, direction=direction, lag_ratio=lag_ratio, dismiss=True, **kwargs)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class PickPage(Animation):
|
|
378
|
+
"""Animate a page sliding out of the stack, then moving to the top/front.
|
|
379
|
+
|
|
380
|
+
The target page slides out in *slide_direction*, pauses visibly beside
|
|
381
|
+
the stack, then slides back to position 0 (the front). The remaining
|
|
382
|
+
pages re-settle to fill the gap.
|
|
383
|
+
|
|
384
|
+
Parameters
|
|
385
|
+
----------
|
|
386
|
+
paper
|
|
387
|
+
The Paper mobject containing the stack.
|
|
388
|
+
page_index
|
|
389
|
+
Which page to pick (0 = current top; 1+ = pages behind it).
|
|
390
|
+
slide_direction
|
|
391
|
+
Direction the page slides out to before returning to top.
|
|
392
|
+
overshoot
|
|
393
|
+
How far (Manim units) the page travels out before settling.
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
def __init__(
|
|
397
|
+
self,
|
|
398
|
+
paper: Paper,
|
|
399
|
+
page_index: int = 1,
|
|
400
|
+
*,
|
|
401
|
+
slide_direction: str | np.ndarray = "RIGHT",
|
|
402
|
+
overshoot: float = 3.0,
|
|
403
|
+
**kwargs: Any,
|
|
404
|
+
) -> None:
|
|
405
|
+
if page_index < 0 or page_index >= paper.page_count:
|
|
406
|
+
raise IndexError(f"page_index {page_index} out of range [0, {paper.page_count})")
|
|
407
|
+
self._paper = paper
|
|
408
|
+
self._page_index = page_index
|
|
409
|
+
self._slide_vec = _parse_direction(slide_direction) * overshoot
|
|
410
|
+
kwargs.setdefault("run_time", 2.0)
|
|
411
|
+
super().__init__(paper, **kwargs)
|
|
412
|
+
|
|
413
|
+
def begin(self) -> None:
|
|
414
|
+
self._page = self._paper.get_page(self._page_index)
|
|
415
|
+
self._start_pos = self._page.get_center().copy()
|
|
416
|
+
|
|
417
|
+
self._other_pages_start: list[np.ndarray] = []
|
|
418
|
+
for i, pg in enumerate(self._paper.page_groups):
|
|
419
|
+
if i != self._page_index:
|
|
420
|
+
self._other_pages_start.append(pg.get_center().copy())
|
|
421
|
+
|
|
422
|
+
self._paper.reorder_page_to_top(self._page_index)
|
|
423
|
+
|
|
424
|
+
self._end_pos = self._paper._stack_dir * self._paper._stack_offset * 0
|
|
425
|
+
self._midpoint = self._start_pos + self._slide_vec
|
|
426
|
+
|
|
427
|
+
self._other_pages_end: list[np.ndarray] = []
|
|
428
|
+
for i, _pg in enumerate(self._paper.page_groups[1:], start=1):
|
|
429
|
+
self._other_pages_end.append(self._paper._stack_dir * self._paper._stack_offset * i)
|
|
430
|
+
|
|
431
|
+
self._other_pages = self._paper.page_groups[1:]
|
|
432
|
+
super().begin()
|
|
433
|
+
|
|
434
|
+
def interpolate_mobject(self, alpha: float) -> None:
|
|
435
|
+
t = smooth(alpha)
|
|
436
|
+
|
|
437
|
+
if t < 0.5:
|
|
438
|
+
sub_t = t * 2.0
|
|
439
|
+
pos = self._start_pos + (self._midpoint - self._start_pos) * sub_t
|
|
440
|
+
else:
|
|
441
|
+
sub_t = (t - 0.5) * 2.0
|
|
442
|
+
pos = self._midpoint + (self._end_pos - self._midpoint) * sub_t
|
|
443
|
+
|
|
444
|
+
self._page.move_to(pos)
|
|
445
|
+
|
|
446
|
+
settle_t = min(t * 2.0, 1.0)
|
|
447
|
+
for i, pg in enumerate(self._other_pages):
|
|
448
|
+
start = self._other_pages_start[i]
|
|
449
|
+
end = self._other_pages_end[i]
|
|
450
|
+
pg.move_to(start + (end - start) * settle_t)
|
|
451
|
+
|
|
452
|
+
def finish(self) -> None:
|
|
453
|
+
self._page.move_to(self._end_pos)
|
|
454
|
+
for i, pg in enumerate(self._other_pages):
|
|
455
|
+
pg.move_to(self._other_pages_end[i])
|
|
456
|
+
super().finish()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Tests for the Paper mobject and its animations."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
pytest.importorskip("manim")
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from simplex.mobjects.paper import (
|
|
12
|
+
DismissPaper,
|
|
13
|
+
Paper,
|
|
14
|
+
PickPage,
|
|
15
|
+
ShowPaper,
|
|
16
|
+
_render_pages,
|
|
17
|
+
_url_to_pdf_url,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def sample_pdf(tmp_path: Path) -> Path:
|
|
23
|
+
"""Create a minimal multi-page PDF for testing."""
|
|
24
|
+
import pymupdf
|
|
25
|
+
|
|
26
|
+
pdf_path = tmp_path / "test.pdf"
|
|
27
|
+
doc = pymupdf.open()
|
|
28
|
+
for i in range(5):
|
|
29
|
+
page = doc.new_page(width=612, height=792)
|
|
30
|
+
tw = pymupdf.TextWriter(page.rect)
|
|
31
|
+
tw.append((72, 72), f"Page {i + 1}", fontsize=24)
|
|
32
|
+
tw.write_text(page)
|
|
33
|
+
doc.save(pdf_path)
|
|
34
|
+
doc.close()
|
|
35
|
+
return pdf_path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_arxiv_url_normalization() -> None:
|
|
39
|
+
assert _url_to_pdf_url("https://arxiv.org/abs/1706.03762") == (
|
|
40
|
+
"https://arxiv.org/pdf/1706.03762.pdf"
|
|
41
|
+
)
|
|
42
|
+
assert _url_to_pdf_url("https://arxiv.org/pdf/1706.03762") == (
|
|
43
|
+
"https://arxiv.org/pdf/1706.03762.pdf"
|
|
44
|
+
)
|
|
45
|
+
assert _url_to_pdf_url("https://arxiv.org/pdf/1706.03762.pdf") == (
|
|
46
|
+
"https://arxiv.org/pdf/1706.03762.pdf"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_render_pages_creates_images(sample_pdf: Path) -> None:
|
|
51
|
+
pages = _render_pages(sample_pdf, pages=3, dpi=72)
|
|
52
|
+
assert len(pages) == 3
|
|
53
|
+
for p in pages:
|
|
54
|
+
assert p.exists()
|
|
55
|
+
assert p.suffix == ".png"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_render_pages_clamps_to_document_length(sample_pdf: Path) -> None:
|
|
59
|
+
pages = _render_pages(sample_pdf, pages=100, dpi=72)
|
|
60
|
+
assert len(pages) == 5
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_paper_constructs_with_local_pdf(sample_pdf: Path) -> None:
|
|
64
|
+
paper = Paper(sample_pdf, pages=3, dpi=72, page_height=4.0)
|
|
65
|
+
assert paper.page_count == 3
|
|
66
|
+
assert len(paper.submobjects) == 3
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_paper_top_page_at_origin(sample_pdf: Path) -> None:
|
|
70
|
+
paper = Paper(sample_pdf, pages=3, dpi=72, page_height=4.0)
|
|
71
|
+
assert np.allclose(paper.get_top_page().get_center(), [0, 0, 0], atol=0.01)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_paper_reorder_to_top(sample_pdf: Path) -> None:
|
|
75
|
+
paper = Paper(sample_pdf, pages=3, dpi=72, page_height=4.0)
|
|
76
|
+
original_back = paper.get_page(2)
|
|
77
|
+
paper.reorder_page_to_top(2)
|
|
78
|
+
assert paper.get_top_page() is original_back
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_show_paper_constructs(sample_pdf: Path) -> None:
|
|
82
|
+
paper = Paper(sample_pdf, pages=2, dpi=72, page_height=3.0)
|
|
83
|
+
anim = ShowPaper(paper, direction="DOWN")
|
|
84
|
+
assert anim.run_time == 1.5
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_dismiss_paper_constructs(sample_pdf: Path) -> None:
|
|
88
|
+
paper = Paper(sample_pdf, pages=2, dpi=72, page_height=3.0)
|
|
89
|
+
anim = DismissPaper(paper, direction="UP")
|
|
90
|
+
assert anim.run_time == 1.5
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_pick_page_constructs(sample_pdf: Path) -> None:
|
|
94
|
+
paper = Paper(sample_pdf, pages=3, dpi=72, page_height=3.0)
|
|
95
|
+
anim = PickPage(paper, page_index=2, slide_direction="RIGHT")
|
|
96
|
+
assert anim.run_time == 2.0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_pick_page_out_of_range_raises(sample_pdf: Path) -> None:
|
|
100
|
+
paper = Paper(sample_pdf, pages=3, dpi=72, page_height=3.0)
|
|
101
|
+
with pytest.raises(IndexError):
|
|
102
|
+
PickPage(paper, page_index=5)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_paper_exit_animation_is_dismiss(sample_pdf: Path) -> None:
|
|
106
|
+
from simplex.engine.animations import exit_for
|
|
107
|
+
|
|
108
|
+
paper = Paper(sample_pdf, pages=2, dpi=72, page_height=3.0)
|
|
109
|
+
anim = exit_for(paper)
|
|
110
|
+
assert isinstance(anim, DismissPaper)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_paper_without_shadow(sample_pdf: Path) -> None:
|
|
114
|
+
paper = Paper(sample_pdf, pages=2, dpi=72, page_height=3.0, shadow=False, border=False)
|
|
115
|
+
assert paper.page_count == 2
|
|
116
|
+
for pg in paper.page_groups:
|
|
117
|
+
assert len(pg.submobjects) == 1
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_paper_with_border_no_shadow(sample_pdf: Path) -> None:
|
|
121
|
+
paper = Paper(sample_pdf, pages=2, dpi=72, page_height=3.0, shadow=False, border=True)
|
|
122
|
+
assert paper.page_count == 2
|
|
123
|
+
for pg in paper.page_groups:
|
|
124
|
+
assert len(pg.submobjects) == 2
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_paper_with_shadow_and_border(sample_pdf: Path) -> None:
|
|
128
|
+
paper = Paper(sample_pdf, pages=2, dpi=72, page_height=3.0, shadow=True, border=True)
|
|
129
|
+
for pg in paper.page_groups:
|
|
130
|
+
assert len(pg.submobjects) == 3
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_dismiss_is_show_subclass(sample_pdf: Path) -> None:
|
|
134
|
+
paper = Paper(sample_pdf, pages=2, dpi=72, page_height=3.0)
|
|
135
|
+
anim = DismissPaper(paper, direction="UP")
|
|
136
|
+
assert isinstance(anim, ShowPaper)
|
|
@@ -443,13 +443,14 @@ wheels = [
|
|
|
443
443
|
|
|
444
444
|
[[package]]
|
|
445
445
|
name = "manim-simplex"
|
|
446
|
-
version = "0.2.
|
|
446
|
+
version = "0.2.2"
|
|
447
447
|
source = { editable = "." }
|
|
448
448
|
dependencies = [
|
|
449
449
|
{ name = "manim" },
|
|
450
450
|
{ name = "manim-slides" },
|
|
451
451
|
{ name = "pydantic" },
|
|
452
452
|
{ name = "pygments" },
|
|
453
|
+
{ name = "pymupdf" },
|
|
453
454
|
]
|
|
454
455
|
|
|
455
456
|
[package.dev-dependencies]
|
|
@@ -467,6 +468,7 @@ requires-dist = [
|
|
|
467
468
|
{ name = "manim-slides", specifier = ">=5.1.7" },
|
|
468
469
|
{ name = "pydantic", specifier = ">=2.7" },
|
|
469
470
|
{ name = "pygments", specifier = ">=2.18" },
|
|
471
|
+
{ name = "pymupdf", specifier = ">=1.27.2.3" },
|
|
470
472
|
]
|
|
471
473
|
|
|
472
474
|
[package.metadata.requires-dev]
|
|
@@ -1016,6 +1018,22 @@ wheels = [
|
|
|
1016
1018
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
1017
1019
|
]
|
|
1018
1020
|
|
|
1021
|
+
[[package]]
|
|
1022
|
+
name = "pymupdf"
|
|
1023
|
+
version = "1.27.2.3"
|
|
1024
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1025
|
+
sdist = { url = "https://files.pythonhosted.org/packages/22/32/708bedc9dde7b328d45abbc076091769d44f2f24ad151ad92d56a6ec142b/pymupdf-1.27.2.3.tar.gz", hash = "sha256:7a92faa25129e8bbec5e50eeb9214f187665428c31b05c4ef6e36c58c0b1c6d2", size = 85759618, upload-time = "2026-04-24T14:13:14.42Z" }
|
|
1026
|
+
wheels = [
|
|
1027
|
+
{ url = "https://files.pythonhosted.org/packages/dc/09/ddbdfa7ee91fbabd6f63d7d744884cbdfe3e7ff9b8604749fb38bddf5c5d/pymupdf-1.27.2.3-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc1bc3cae6e9e150b0dbb0a9221bdfd411d65f0db2fe359eaa22467d7cc2a05f", size = 24002636, upload-time = "2026-04-24T14:09:17.459Z" },
|
|
1028
|
+
{ url = "https://files.pythonhosted.org/packages/01/89/3f8edd6c4f50ca370e2a2f2a3011face36f3760728ffe76dffec91c0fca0/pymupdf-1.27.2.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:660d93cb6da5bbddf11d3982ae27745dd3a9902d9f24cdb69adab83962294b5a", size = 23278238, upload-time = "2026-04-24T14:09:32.882Z" },
|
|
1029
|
+
{ url = "https://files.pythonhosted.org/packages/c3/26/b7e5a70eb83bd189f8b5df87ec442746b992f2f632662839b288170d357d/pymupdf-1.27.2.3-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1dd460a3ae4597a755f00a3bd9771f5ebf1531dc111f6a36bf05dd00a6b84425", size = 24333923, upload-time = "2026-04-24T14:09:47.341Z" },
|
|
1030
|
+
{ url = "https://files.pythonhosted.org/packages/e4/a0/aa1ee2240f29481a04a827c313333b4ecd8a14d6ac3e15d3f41a30574781/pymupdf-1.27.2.3-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:857842b4888827bd6155a1131341b2822a7ebe9a8c15a975fd7d490d7a64a30c", size = 24963198, upload-time = "2026-04-24T14:10:07.408Z" },
|
|
1031
|
+
{ url = "https://files.pythonhosted.org/packages/69/49/4f742451f980840829fc00ba158bebb25d389c846d8f4f8c65936ee55de8/pymupdf-1.27.2.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:580983849c64a08d08344ca3d1580e87c01f046a8392421797bc850efd72a5b6", size = 25184609, upload-time = "2026-04-24T14:10:22.911Z" },
|
|
1032
|
+
{ url = "https://files.pythonhosted.org/packages/f6/3f/3853d6608f394faf6eec2bd4e8ea9f6a00beea329b071abdb29f4164cc3d/pymupdf-1.27.2.3-cp310-abi3-win32.whl", hash = "sha256:a5c1088a87189891a4946ab314a14b7934ac4c5b6077f7e74ebee956f8906d0e", size = 18019286, upload-time = "2026-04-24T14:10:34.239Z" },
|
|
1033
|
+
{ url = "https://files.pythonhosted.org/packages/44/47/5fb10fe73f96b31253a41647c362ea9e0380920bddf16028414a051247fc/pymupdf-1.27.2.3-cp310-abi3-win_amd64.whl", hash = "sha256:d20f68ef15195e073071dbc4ae7455257c7889af7584e39df490c0a92728526e", size = 19249102, upload-time = "2026-04-24T14:10:46.72Z" },
|
|
1034
|
+
{ url = "https://files.pythonhosted.org/packages/53/a4/b9e91aac82293f9c954654c85581ee8212b5b05efadc534b581141241e6f/pymupdf-1.27.2.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:77691604c5d1d0233827139bbcdea61fd57879c84712b8e49b1f45520f7ab9c2", size = 25000393, upload-time = "2026-04-24T14:11:01.669Z" },
|
|
1035
|
+
]
|
|
1036
|
+
|
|
1019
1037
|
[[package]]
|
|
1020
1038
|
name = "pyobjc-core"
|
|
1021
1039
|
version = "12.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|