Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(convert): PowerPoint conversion #152

Merged
merged 12 commits into from
Mar 9, 2023
Merged
25 changes: 25 additions & 0 deletions docs/source/features_table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Features Table

The following summarizes the different presentation features Manim Slides offers.

:::{table} Comparison of the different presentation methods.
:widths: auto
:align: center

| Feature / Constraint | [`present`](reference/cli.md) | [`convert --to=html`](reference/cli.md) | [`convert --to=pptx`](reference/cli.md) |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LanguageTool] reported by reviewdog 🐶
File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
Suggestions: HTML
URL: https://languagetool.org/insights/post/spelling-capital-letters/
Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
Category: CASING

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LanguageTool] reported by reviewdog 🐶
File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
Suggestions: PPTX
URL: https://languagetool.org/insights/post/spelling-capital-letters/
Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
Category: CASING

| :--- | :---: | :---: | :---: |
| Basic navigation through slides | Yes | Yes | Yes |
| Replay slide | Yes | No | No |
| Pause animation | Yes | No | No |
| Play slide in reverse | 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] |
:::

[^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).
[^2]: PowerPoint online does not seem to support automatic playing of videos, so you need LibreOffice Impress on Linux platforms.
1 change: 1 addition & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Slide through the demo below to get a quick glimpse on what you can do with Mani

quickstart
reference/index
features_table
```

```{toctree}
Expand Down
14 changes: 14 additions & 0 deletions docs/source/reference/sharing.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,17 @@ to know how to share your slide with GitHub pages, see the
> **WARNING:** keep in mind that playing large video files over the internet
can take some time, and *glitches* may occur between slide transitions for this
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.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LanguageTool] reported by reviewdog 🐶
File types are normally capitalized. (FILE_EXTENSIONS_CASE[1])
Suggestions: PPTX
URL: https://languagetool.org/insights/post/spelling-capital-letters/
Rule: https://community.languagetool.org/rule/show/FILE_EXTENSIONS_CASE?lang=en-US&subId=1
Category: CASING


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

In the future, we hope to provide more features to this format, so feel free to suggest new features too!
107 changes: 105 additions & 2 deletions manim_slides/convert.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
import os
import platform
import subprocess
import tempfile
import webbrowser
from enum import Enum
from pathlib import Path
from typing import Any, Callable, Dict, Generator, List, Optional, Type, Union

import click
import cv2
import pkg_resources
import pptx
from click import Context, Parameter
from pydantic import BaseModel, PositiveInt, ValidationError
from lxml import etree
from pydantic import BaseModel, FilePath, PositiveInt, ValidationError
from tqdm import tqdm

from .commons import folder_path_option, verbosity_option
from .config import PresentationConfig
from .logger import logger
from .present import get_scenes_presentation_config


def open_with_default(file: Path):
system = platform.system()
if system == "Darwin":
subprocess.call(("open", str(file)))
elif system == "Windows":
os.startfile(str(file)) # type: ignore[attr-defined]
else:
subprocess.call(("xdg-open", str(file)))


def validate_config_option(
ctx: Context, param: Parameter, value: Any
) -> Dict[str, str]:
Expand Down Expand Up @@ -55,6 +73,7 @@ def from_string(cls, s: str) -> Type["Converter"]:
"""Returns the appropriate converter from a string name."""
return {
"html": RevealJS,
"pptx": PowerPoint,
}[s]


Expand Down Expand Up @@ -341,6 +360,90 @@ def convert_to(self, dest: Path) -> None:
f.write(content)


class PowerPoint(Converter):
left: PositiveInt = 0
top: PositiveInt = 0
width: PositiveInt = 1280
height: PositiveInt = 720
auto_play_media: bool = True
poster_frame_image: Optional[FilePath] = None

class Config:
use_enum_values = True
extra = "forbid"

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

def convert_to(self, dest: Path) -> None:
"""Converts this configuration into a PowerPoint presentation, saved to DEST."""
prs = pptx.Presentation()
prs.slide_width = self.width * 9525
prs.slide_height = self.height * 9525

layout = prs.slide_layouts[6] # Should be blank

# From GitHub issue comment:
# - https://github.com/scanny/python-pptx/issues/427#issuecomment-856724440
def auto_play_media(media: pptx.shape.picture.Movie, loop: bool = False):
el_id = xpath(media.element, ".//p:cNvPr")[0].attrib["id"]
el_cnt = xpath(
media.element.getparent().getparent().getparent(),
'.//p:timing//p:video//p:spTgt[@spid="%s"]' % el_id,
)[0]
cond = xpath(el_cnt.getparent().getparent(), ".//p:cond")[0]
cond.set("delay", "0")

if loop:
ctn = xpath(el_cnt.getparent().getparent(), ".//p:cTn")[0]
ctn.set("repeatCount", "indefinite")

def xpath(el: etree.Element, query: str) -> etree.XPath:
nsmap = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main"}
return etree.ElementBase.xpath(el, query, namespaces=nsmap)

def save_first_image_from_video_file(file: Path) -> Optional[str]:
cap = cv2.VideoCapture(str(file))
ret, frame = cap.read()

if ret:
f = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".png")
cv2.imwrite(f.name, frame)
return f.name
else:
logger.warn("Failed to read first image from video file")
return None

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]

if self.poster_frame_image is None:
poster_frame_image = save_first_image_from_video_file(file)
else:
poster_frame_image = str(self.poster_frame_image)

slide = prs.slides.add_slide(layout)
movie = slide.shapes.add_movie(
str(file),
self.left,
self.top,
self.width * 9525,
self.height * 9525,
poster_frame_image=poster_frame_image,
mime_type="video/mp4",
)
if self.auto_play_media:
auto_play_media(movie, loop=slide_config.is_loop())

prs.save(dest)


def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wraps a function to add a `--show-config` option."""

Expand Down Expand Up @@ -401,7 +504,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"], case_sensitive=False),
type=click.Choice(["html", "pptx"], case_sensitive=False),
default="html",
show_default=True,
help="Set the conversion format to use.",
Expand Down
Loading