Skip to content

Commit c7e38bf

Browse files
authored
feat(convert): add to PDF conversion (#197)
* feat(convert): add to PDF conversion Basic PDF conversion. It takes the last frame (by default) for each animation, and prints out a PDF page. Closes #196 * chore(ci): remove experimental installer * feat(convert): add to PDF conversion Basic PDF conversion. It takes the last frame (by default) for each animation, and prints out a PDF page. Closes #196 * feat(convert): add to PDF conversion Basic PDF conversion. It takes the last frame (by default) for each animation, and prints out a PDF page. Closes #196 * chore(deps): update lockfile
1 parent 421cad3 commit c7e38bf

File tree

5 files changed

+1719
-1456
lines changed

5 files changed

+1719
-1456
lines changed

docs/source/features_table.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,21 @@ The following summarizes the different presentation features Manim Slides offers
66
:widths: auto
77
:align: center
88

9-
| Feature / Constraint | [`present`](reference/cli.md) | [`convert --to=html`](reference/cli.md) | [`convert --to=pptx`](reference/cli.md) |
10-
| :--- | :---: | :---: | :---: |
11-
| Basic navigation through slides | Yes | Yes | Yes |
12-
| Replay slide | Yes | No | No |
13-
| Pause animation | Yes | No | No |
14-
| Play slide in reverse | Yes | No | No |
15-
| Slide count | Yes | Yes (optional) | Yes (optional) |
16-
| Animation count | Yes | No | No |
17-
| Needs Python with Manim Slides installed | Yes | No | No |
18-
| Requires internet access | No | Yes | No |
19-
| Auto. play slides | Yes | Yes | Yes |
20-
| Loops support | Yes | Yes | Yes |
21-
| Fully customizable | No | Yes (`--use-template` option) | No |
22-
| Other dependencies | None | A modern web browser | PowerPoint or LibreOffice Impress[^1]
23-
| Works cross-platforms | Yes | Yes | Partly[^1][^2] |
9+
| Feature / Constraint | [`present`](reference/cli.md) | [`convert --to=html`](reference/cli.md) | [`convert --to=pptx`](reference/cli.md) | [`convert --to=pdf`](reference/cli.md)
10+
| :--- | :---: | :---: | :---: | :---: |
11+
| Basic navigation through slides | Yes | Yes | Yes | Yes (static image) |
12+
| Replay slide | Yes | No | No | N/A |
13+
| Pause animation | Yes | No | No | N/A |
14+
| Play slide in reverse | Yes | No | No | N/A |
15+
| Slide count | Yes | Yes (optional) | Yes (optional) | N/A |
16+
| Animation count | Yes | No | No | N/A |
17+
| Needs Python with Manim Slides installed | Yes | No | No | No
18+
| Requires internet access | No | Yes | No | No |
19+
| Auto. play slides | Yes | Yes | Yes | N/A |
20+
| Loops support | Yes | Yes | Yes | N/A |
21+
| Fully customizable | No | Yes (`--use-template` option) | No | No |
22+
| Other dependencies | None | A modern web browser | PowerPoint or LibreOffice Impress[^1] | None |
23+
| Works cross-platforms | Yes | Yes | Partly[^1][^2] | Yes |
2424
:::
2525

2626
[^1]: If you encounter a problem where slides do not automatically play or loops do not work, please [file an issue on GitHub](https://github.com/jeertmans/manim-slides/issues/new/choose).

docs/source/reference/sharing.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,35 @@ reason.
150150

151151
### With PowerPoint (*EXPERIMENTAL*)
152152

153-
A recent conversion feature is to the PowerPoint format, thanks to the `python-pptx` package. Even though it is fully working, it is still considered in an *EXPERIMENTAL* status because we do not exactly know what versions of PowerPoint (or LibreOffice Impress) are supported.
153+
A recent conversion feature is to the PowerPoint format, thanks to the
154+
`python-pptx` package. Even though it is fully working,
155+
it is still considered in an *EXPERIMENTAL* status because we do not
156+
exactly know what versions of PowerPoint (or LibreOffice Impress) are supported.
154157

155158
Basically, you can create a PowerPoint in a single command:
156159

157160
```bash
158161
manim-slides convert --to=pptx BasicExample basic_example.pptx
159162
```
160163

161-
All the videos and necessary files will be contained inside the `.pptx` file, so you can safely share it with anyone. By default, the `poster_frame_image`, i.e., what is displayed by PowerPoint when the video is not playing, is the first frame of each slide. This allows for smooth transitions.
164+
All the videos and necessary files will be contained inside the `.pptx` file, so
165+
you can safely share it with anyone. By default, the `poster_frame_image`, i.e.,
166+
what is displayed by PowerPoint when the video is not playing, is the first
167+
frame of each slide. This allows for smooth transitions.
162168

163-
In the future, we hope to provide more features to this format, so feel free to suggest new features too!
169+
In the future, we hope to provide more features to this format,
170+
so feel free to suggest new features too!
171+
172+
### Static PDF presentation
173+
174+
If you ever need backup slides, that are only made of PDF pages
175+
with static images, you can generate such a PDF with the following command:
176+
177+
```bash
178+
manim-slides convert --to=pdf BasicExample basic_example.pdf
179+
```
180+
181+
Note that you will lose all the benefits from animated slides. Therefore,
182+
this is only recommended to be used as a backup plan. By default, the last frame
183+
of each slide will be printed. This can be changed to be the first one with
184+
`-cframe_index=first`.

manim_slides/convert.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
import pptx
1515
from click import Context, Parameter
1616
from lxml import etree
17-
from pydantic import BaseModel, FilePath, PositiveInt, ValidationError
17+
from PIL import Image
18+
from pydantic import BaseModel, FilePath, PositiveFloat, PositiveInt, ValidationError
1819
from tqdm import tqdm
1920

2021
from . import data
@@ -75,6 +76,7 @@ def from_string(cls, s: str) -> Type["Converter"]:
7576
"""Returns the appropriate converter from a string name."""
7677
return {
7778
"html": RevealJS,
79+
"pdf": PDF,
7880
"pptx": PowerPoint,
7981
}[s]
8082

@@ -367,6 +369,62 @@ def convert_to(self, dest: Path) -> None:
367369
f.write(content)
368370

369371

372+
class FrameIndex(str, Enum):
373+
first = "first"
374+
last = "last"
375+
376+
377+
class PDF(Converter):
378+
frame_index: FrameIndex = FrameIndex.last
379+
resolution: PositiveFloat = 100.0
380+
381+
class Config:
382+
use_enum_values = True
383+
extra = "forbid"
384+
385+
def open(self, file: Path) -> None:
386+
return open_with_default(file)
387+
388+
def convert_to(self, dest: Path) -> None:
389+
"""Converts this configuration into a PDF presentation, saved to DEST."""
390+
391+
def read_image_from_video_file(file: Path, frame_index: FrameIndex) -> Image:
392+
cap = cv2.VideoCapture(str(file))
393+
394+
if frame_index == FrameIndex.last:
395+
index = cap.get(cv2.CAP_PROP_FRAME_COUNT)
396+
cap.set(cv2.CAP_PROP_POS_FRAMES, index - 1)
397+
398+
ret, frame = cap.read()
399+
400+
if ret:
401+
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
402+
return Image.fromarray(frame)
403+
else:
404+
raise ValueError("Failed to read {image_index} image from video file")
405+
406+
images = []
407+
408+
for i, presentation_config in enumerate(self.presentation_configs):
409+
presentation_config.concat_animations()
410+
for slide_config in tqdm(
411+
presentation_config.slides,
412+
desc=f"Generating video slides for config {i + 1}",
413+
leave=False,
414+
):
415+
file = presentation_config.files[slide_config.start_animation]
416+
417+
images.append(read_image_from_video_file(file, self.frame_index))
418+
419+
images[0].save(
420+
dest,
421+
"PDF",
422+
resolution=self.resolution,
423+
save_all=True,
424+
append_images=images[1:],
425+
)
426+
427+
370428
class PowerPoint(Converter):
371429
left: PositiveInt = 0
372430
top: PositiveInt = 0
@@ -513,7 +571,7 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None:
513571
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
514572
@click.option(
515573
"--to",
516-
type=click.Choice(["html", "pptx"], case_sensitive=False),
574+
type=click.Choice(["html", "pdf", "pptx"], case_sensitive=False),
517575
default="html",
518576
show_default=True,
519577
help="Set the conversion format to use.",

0 commit comments

Comments
 (0)