Skip to content

Commit

Permalink
feat(convert): add to PDF conversion (#197)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jeertmans authored Jun 12, 2023
1 parent 421cad3 commit c7e38bf
Show file tree
Hide file tree
Showing 5 changed files with 1,719 additions and 1,456 deletions.
30 changes: 15 additions & 15 deletions docs/source/features_table.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ The following summarizes the different presentation features Manim Slides offers
:widths: auto
:align: center

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

[^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).
Expand Down
27 changes: 24 additions & 3 deletions docs/source/reference/sharing.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,35 @@ reason.

### With PowerPoint (*EXPERIMENTAL*)

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.
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.

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

```bash
manim-slides convert --to=pptx BasicExample basic_example.pptx
```

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.
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.

In the future, we hope to provide more features to this format, so feel free to suggest new features too!
In the future, we hope to provide more features to this format,
so feel free to suggest new features too!

### Static PDF presentation

If you ever need backup slides, that are only made of PDF pages
with static images, you can generate such a PDF with the following command:

```bash
manim-slides convert --to=pdf BasicExample basic_example.pdf
```

Note that you will lose all the benefits from animated slides. Therefore,
this is only recommended to be used as a backup plan. By default, the last frame
of each slide will be printed. This can be changed to be the first one with
`-cframe_index=first`.
62 changes: 60 additions & 2 deletions manim_slides/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
import pptx
from click import Context, Parameter
from lxml import etree
from pydantic import BaseModel, FilePath, PositiveInt, ValidationError
from PIL import Image
from pydantic import BaseModel, FilePath, PositiveFloat, PositiveInt, ValidationError
from tqdm import tqdm

from . import data
Expand Down Expand Up @@ -75,6 +76,7 @@ def from_string(cls, s: str) -> Type["Converter"]:
"""Returns the appropriate converter from a string name."""
return {
"html": RevealJS,
"pdf": PDF,
"pptx": PowerPoint,
}[s]

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


class FrameIndex(str, Enum):
first = "first"
last = "last"


class PDF(Converter):
frame_index: FrameIndex = FrameIndex.last
resolution: PositiveFloat = 100.0

class Config:
use_enum_values = True
extra = "forbid"

def open(self, file: Path) -> None:
return open_with_default(file)

def convert_to(self, dest: Path) -> None:
"""Converts this configuration into a PDF presentation, saved to DEST."""

def read_image_from_video_file(file: Path, frame_index: FrameIndex) -> Image:
cap = cv2.VideoCapture(str(file))

if frame_index == FrameIndex.last:
index = cap.get(cv2.CAP_PROP_FRAME_COUNT)
cap.set(cv2.CAP_PROP_POS_FRAMES, index - 1)

ret, frame = cap.read()

if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
return Image.fromarray(frame)
else:
raise ValueError("Failed to read {image_index} image from video file")

images = []

for i, presentation_config in enumerate(self.presentation_configs):
presentation_config.concat_animations()
for slide_config in tqdm(
presentation_config.slides,
desc=f"Generating video slides for config {i + 1}",
leave=False,
):
file = presentation_config.files[slide_config.start_animation]

images.append(read_image_from_video_file(file, self.frame_index))

images[0].save(
dest,
"PDF",
resolution=self.resolution,
save_all=True,
append_images=images[1:],
)


class PowerPoint(Converter):
left: PositiveInt = 0
top: PositiveInt = 0
Expand Down Expand Up @@ -513,7 +571,7 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None:
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
@click.option(
"--to",
type=click.Choice(["html", "pptx"], case_sensitive=False),
type=click.Choice(["html", "pdf", "pptx"], case_sensitive=False),
default="html",
show_default=True,
help="Set the conversion format to use.",
Expand Down
Loading

0 comments on commit c7e38bf

Please sign in to comment.