mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-07-15 00:52:15 +08:00
Merge branch 'main' into reorganize
This commit is contained in:
@ -1,8 +1,11 @@
|
||||
import mimetypes
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import textwrap
|
||||
import warnings
|
||||
import webbrowser
|
||||
from base64 import b64encode
|
||||
from collections import deque
|
||||
@ -14,6 +17,8 @@ from typing import Any, Callable, Optional, Union
|
||||
import av
|
||||
import click
|
||||
import pptx
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from click import Context, Parameter
|
||||
from jinja2 import Template
|
||||
from lxml import etree
|
||||
@ -27,7 +32,6 @@ from pydantic import (
|
||||
PositiveFloat,
|
||||
PositiveInt,
|
||||
ValidationError,
|
||||
conlist,
|
||||
)
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
from pydantic_extra_types.color import Color
|
||||
@ -103,9 +107,12 @@ def read_image_from_video_file(file: Path, frame_index: "FrameIndex") -> Image:
|
||||
|
||||
|
||||
class Converter(BaseModel): # type: ignore
|
||||
presentation_configs: conlist(PresentationConfig, min_length=1) # type: ignore[valid-type]
|
||||
assets_dir: str = "{basename}_assets"
|
||||
template: Optional[Path] = None
|
||||
presentation_configs: list[PresentationConfig]
|
||||
assets_dir: str = Field(
|
||||
"{basename}_assets",
|
||||
description="Assets folder.\nThis is a template string that accepts 'dirname', 'basename', and 'ext' as variables.\nThose variables are obtained from the output filename.",
|
||||
)
|
||||
template: Optional[Path] = Field(None, description="Custom template file to use.")
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""Convert self, i.e., a list of presentations, into a given format."""
|
||||
@ -119,9 +126,9 @@ class Converter(BaseModel): # type: ignore
|
||||
"""
|
||||
return ""
|
||||
|
||||
def open(self, file: Path) -> Any:
|
||||
def open(self, file: Path) -> None:
|
||||
"""Open a file, generated with converter, using appropriate application."""
|
||||
raise NotImplementedError
|
||||
open_with_default(file)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, s: str) -> type["Converter"]:
|
||||
@ -130,6 +137,7 @@ class Converter(BaseModel): # type: ignore
|
||||
"html": RevealJS,
|
||||
"pdf": PDF,
|
||||
"pptx": PowerPoint,
|
||||
"zip": HtmlZip,
|
||||
}[s]
|
||||
|
||||
|
||||
@ -290,49 +298,153 @@ class RevealTheme(str, StrEnum):
|
||||
|
||||
|
||||
class RevealJS(Converter):
|
||||
# Export option: use data-uri
|
||||
data_uri: bool = False
|
||||
"""
|
||||
RevealJS options.
|
||||
|
||||
Please check out https://revealjs.com/config/ for more details.
|
||||
"""
|
||||
|
||||
# Export option:
|
||||
one_file: bool = Field(
|
||||
False, description="Embed all assets (e.g., animations) inside the HTML."
|
||||
)
|
||||
offline: bool = Field(
|
||||
False, description="Download remote assets for offline presentation."
|
||||
)
|
||||
# Presentation size options from RevealJS
|
||||
width: Union[Str, int] = Str("100%")
|
||||
height: Union[Str, int] = Str("100%")
|
||||
margin: float = 0.04
|
||||
min_scale: float = 0.2
|
||||
max_scale: float = 2.0
|
||||
width: Union[Str, int] = Field(
|
||||
Str("100%"), description="Width of the presentation."
|
||||
)
|
||||
height: Union[Str, int] = Field(
|
||||
Str("100%"), description="Height of the presentation."
|
||||
)
|
||||
margin: float = Field(0.04, description="Margin to use around the content.")
|
||||
min_scale: float = Field(
|
||||
0.2, description="Bound for smallest possible scale to apply to content."
|
||||
)
|
||||
max_scale: float = Field(
|
||||
2.0, description="Bound for large possible scale to apply to content."
|
||||
)
|
||||
# Configuration options from RevealJS
|
||||
controls: JsBool = JsBool.false
|
||||
controls_tutorial: JsBool = JsBool.true
|
||||
controls_layout: ControlsLayout = ControlsLayout.bottom_right
|
||||
controls_back_arrows: ControlsBackArrows = ControlsBackArrows.faded
|
||||
progress: JsBool = JsBool.false
|
||||
slide_number: SlideNumber = SlideNumber.false
|
||||
show_slide_number: Union[ShowSlideNumber, Function] = ShowSlideNumber.all
|
||||
hash_one_based_index: JsBool = JsBool.false
|
||||
hash: JsBool = JsBool.false
|
||||
respond_to_hash_changes: JsBool = JsBool.false
|
||||
history: JsBool = JsBool.false
|
||||
keyboard: JsBool = JsBool.true
|
||||
keyboard_condition: Union[KeyboardCondition, Function] = KeyboardCondition.null
|
||||
disable_layout: JsBool = JsBool.false
|
||||
overview: JsBool = JsBool.true
|
||||
center: JsBool = JsBool.true
|
||||
touch: JsBool = JsBool.true
|
||||
loop: JsBool = JsBool.false
|
||||
rtl: JsBool = JsBool.false
|
||||
navigation_mode: NavigationMode = NavigationMode.default
|
||||
shuffle: JsBool = JsBool.false
|
||||
fragments: JsBool = JsBool.true
|
||||
fragment_in_url: JsBool = JsBool.true
|
||||
embedded: JsBool = JsBool.false
|
||||
help: JsBool = JsBool.true
|
||||
pause: JsBool = JsBool.true
|
||||
show_notes: JsBool = JsBool.false
|
||||
auto_play_media: AutoPlayMedia = AutoPlayMedia.null
|
||||
preload_iframes: PreloadIframes = PreloadIframes.null
|
||||
auto_animate: JsBool = JsBool.true
|
||||
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = AutoAnimateMatcher.null
|
||||
auto_animate_easing: AutoAnimateEasing = AutoAnimateEasing.ease
|
||||
auto_animate_duration: float = 1.0
|
||||
auto_animate_unmatched: JsBool = JsBool.true
|
||||
controls: JsBool = Field(
|
||||
JsBool.false, description="Display presentation control arrows."
|
||||
)
|
||||
controls_tutorial: JsBool = Field(
|
||||
JsBool.true, description="Help the user learn the controls by providing hints."
|
||||
)
|
||||
controls_layout: ControlsLayout = Field(
|
||||
ControlsLayout.bottom_right, description="Determine where controls appear."
|
||||
)
|
||||
controls_back_arrows: ControlsBackArrows = Field(
|
||||
ControlsBackArrows.faded,
|
||||
description="Visibility rule for backwards navigation arrows.",
|
||||
)
|
||||
progress: JsBool = Field(
|
||||
JsBool.false, description="Display a presentation progress bar."
|
||||
)
|
||||
slide_number: SlideNumber = Field(
|
||||
SlideNumber.false, description="Display the page number of the current slide."
|
||||
)
|
||||
show_slide_number: Union[ShowSlideNumber, Function] = Field(
|
||||
ShowSlideNumber.all,
|
||||
description="Can be used to limit the contexts in which the slide number appears.",
|
||||
)
|
||||
hash_one_based_index: JsBool = Field(
|
||||
JsBool.false,
|
||||
description="Use 1 based indexing for # links to match slide number (default is zero based).",
|
||||
)
|
||||
hash: JsBool = Field(
|
||||
JsBool.false,
|
||||
description="Add the current slide number to the URL hash so that reloading the page/copying the URL will return you to the same slide.",
|
||||
)
|
||||
respond_to_hash_changes: JsBool = Field(
|
||||
JsBool.false,
|
||||
description="Flags if we should monitor the hash and change slides accordingly.",
|
||||
)
|
||||
jump_to_slide: JsBool = Field(
|
||||
JsBool.true,
|
||||
description="Enable support for jump-to-slide navigation shortcuts.",
|
||||
)
|
||||
history: JsBool = Field(
|
||||
JsBool.false,
|
||||
description="Push each slide change to the browser history. Implies `hash: true`.",
|
||||
)
|
||||
keyboard: JsBool = Field(
|
||||
JsBool.true, description="Enable keyboard shortcuts for navigation."
|
||||
)
|
||||
keyboard_condition: Union[KeyboardCondition, Function] = Field(
|
||||
KeyboardCondition.null,
|
||||
description="Optional function that blocks keyboard events when retuning false.",
|
||||
)
|
||||
disable_layout: JsBool = Field(
|
||||
JsBool.false,
|
||||
description="Disable the default reveal.js slide layout (scaling and centering) so that you can use custom CSS layout.",
|
||||
)
|
||||
overview: JsBool = Field(JsBool.true, description="Enable the slide overview mode.")
|
||||
center: JsBool = Field(JsBool.true, description="Vertical centering of slides.")
|
||||
touch: JsBool = Field(
|
||||
JsBool.true, description="Enable touch navigation on devices with touch input."
|
||||
)
|
||||
loop: JsBool = Field(JsBool.false, description="Loop the presentation.")
|
||||
rtl: JsBool = Field(
|
||||
JsBool.false, description="Change the presentation direction to be RTL."
|
||||
)
|
||||
navigation_mode: NavigationMode = Field(
|
||||
NavigationMode.default,
|
||||
description="Change the behavior of our navigation directions.",
|
||||
)
|
||||
shuffle: JsBool = Field(
|
||||
JsBool.false,
|
||||
description="Randomize the order of slides each time the presentation loads.",
|
||||
)
|
||||
fragments: JsBool = Field(
|
||||
JsBool.true, description="Turns fragment on and off globally."
|
||||
)
|
||||
fragment_in_url: JsBool = Field(
|
||||
JsBool.true,
|
||||
description="Flag whether to include the current fragment in the URL, so that reloading brings you to the same fragment position.",
|
||||
)
|
||||
embedded: JsBool = Field(
|
||||
JsBool.false,
|
||||
description="Flag if the presentation is running in an embedded mode, i.e. contained within a limited portion of the screen.",
|
||||
)
|
||||
help: JsBool = Field(
|
||||
JsBool.true,
|
||||
description="Flag if we should show a help overlay when the question-mark key is pressed.",
|
||||
)
|
||||
pause: JsBool = Field(
|
||||
JsBool.true,
|
||||
description="Flag if it should be possible to pause the presentation (blackout).",
|
||||
)
|
||||
show_notes: JsBool = Field(
|
||||
JsBool.false,
|
||||
description="Flag if speaker notes should be visible to all viewers.",
|
||||
)
|
||||
auto_play_media: AutoPlayMedia = Field(
|
||||
AutoPlayMedia.null,
|
||||
description="Global override for autolaying embedded media (video/audio/iframe).",
|
||||
)
|
||||
preload_iframes: PreloadIframes = Field(
|
||||
PreloadIframes.null,
|
||||
description="Global override for preloading lazy-loaded iframes.",
|
||||
)
|
||||
auto_animate: JsBool = Field(
|
||||
JsBool.true, description="Can be used to globally disable auto-animation."
|
||||
)
|
||||
auto_animate_matcher: Union[AutoAnimateMatcher, Function] = Field(
|
||||
AutoAnimateMatcher.null,
|
||||
description="Optionally provide a custom element matcher that will be used to dictate which elements we can animate between.",
|
||||
)
|
||||
auto_animate_easing: AutoAnimateEasing = Field(
|
||||
AutoAnimateEasing.ease,
|
||||
description="Default settings for our auto-animate transitions, can be overridden per-slide or per-element via data arguments.",
|
||||
)
|
||||
auto_animate_duration: float = Field(
|
||||
1.0, description="See 'auto_animate_easing' documentation."
|
||||
)
|
||||
auto_animate_unmatched: JsBool = Field(
|
||||
JsBool.true, description="See 'auto_animate_easing' documentation."
|
||||
)
|
||||
auto_animate_styles: list[str] = Field(
|
||||
default_factory=lambda: [
|
||||
"opacity",
|
||||
@ -347,34 +459,88 @@ class RevealJS(Converter):
|
||||
"border-radius",
|
||||
"outline",
|
||||
"outline-offset",
|
||||
]
|
||||
],
|
||||
description="CSS properties that can be auto-animated.",
|
||||
)
|
||||
auto_slide: AutoSlide = Field(
|
||||
0, description="Control automatic progression to the next slide."
|
||||
)
|
||||
auto_slide_stoppable: JsBool = Field(
|
||||
JsBool.true, description="Stop auto-sliding after user input."
|
||||
)
|
||||
auto_slide_method: Union[AutoSlideMethod, Function] = Field(
|
||||
AutoSlideMethod.null,
|
||||
description="Use this method for navigation when auto-sliding (defaults to navigateNext).",
|
||||
)
|
||||
default_timing: Union[JsNull, int] = Field(
|
||||
JsNull.null,
|
||||
description="Specify the average time in seconds that you think you will spend presenting each slide.",
|
||||
)
|
||||
mouse_wheel: JsBool = Field(
|
||||
JsBool.false, description="Enable slide navigation via mouse wheel."
|
||||
)
|
||||
preview_links: JsBool = Field(
|
||||
JsBool.false, description="Open links in an iframe preview overlay."
|
||||
)
|
||||
post_message: JsBool = Field(
|
||||
JsBool.true, description="Expose the reveal.js API through window.postMessage."
|
||||
)
|
||||
post_message_events: JsBool = Field(
|
||||
JsBool.false,
|
||||
description="Dispatch all reveal.js events to the parent window through postMessage.",
|
||||
)
|
||||
focus_body_on_page_visibility_change: JsBool = Field(
|
||||
JsBool.true,
|
||||
description="Focus body when page changes visibility to ensure keyboard shortcuts work.",
|
||||
)
|
||||
transition: Transition = Field(Transition.none, description="Transition style.")
|
||||
transition_speed: TransitionSpeed = Field(
|
||||
TransitionSpeed.default, description="Transition speed."
|
||||
)
|
||||
background_size: BackgroundSize = Field(
|
||||
BackgroundSize.contain, description="Background size attribute for each video."
|
||||
) # Not in RevealJS
|
||||
background_transition: BackgroundTransition = Field(
|
||||
BackgroundTransition.none,
|
||||
description="Transition style for full page slide backgrounds.",
|
||||
)
|
||||
pdf_max_pages_per_slide: Union[int, str] = Field(
|
||||
"Number.POSITIVE_INFINITY",
|
||||
description="The maximum number of pages a single slide can expand onto when printing to PDF, unlimited by default.",
|
||||
)
|
||||
pdf_separate_fragments: JsBool = Field(
|
||||
JsBool.true, description="Print each fragment on a separate slide."
|
||||
)
|
||||
pdf_page_height_offset: int = Field(
|
||||
-1,
|
||||
description="Offset used to reduce the height of content within exported PDF pages.",
|
||||
)
|
||||
view_distance: int = Field(
|
||||
3, description="Number of slides away from the current that are visible."
|
||||
)
|
||||
mobile_view_distance: int = Field(
|
||||
2,
|
||||
description="Number of slides away from the current that are visible on mobile devices.",
|
||||
)
|
||||
display: Display = Field(
|
||||
Display.block, description="The display mode that will be used to show slides."
|
||||
)
|
||||
hide_inactive_cursor: JsBool = Field(
|
||||
JsBool.true, description="Hide cursor if inactive."
|
||||
)
|
||||
hide_cursor_time: int = Field(
|
||||
5000, description="Time before the cursor is hidden (in ms)."
|
||||
)
|
||||
auto_slide: AutoSlide = 0
|
||||
auto_slide_stoppable: JsBool = JsBool.true
|
||||
auto_slide_method: Union[AutoSlideMethod, Function] = AutoSlideMethod.null
|
||||
default_timing: Union[JsNull, int] = JsNull.null
|
||||
mouse_wheel: JsBool = JsBool.false
|
||||
preview_links: JsBool = JsBool.false
|
||||
post_message: JsBool = JsBool.true
|
||||
post_message_events: JsBool = JsBool.false
|
||||
focus_body_on_page_visibility_change: JsBool = JsBool.true
|
||||
transition: Transition = Transition.none
|
||||
transition_speed: TransitionSpeed = TransitionSpeed.default
|
||||
background_size: BackgroundSize = BackgroundSize.contain # Not in RevealJS
|
||||
background_transition: BackgroundTransition = BackgroundTransition.none
|
||||
pdf_max_pages_per_slide: Union[int, str] = "Number.POSITIVE_INFINITY"
|
||||
pdf_separate_fragments: JsBool = JsBool.true
|
||||
pdf_page_height_offset: int = -1
|
||||
view_distance: int = 3
|
||||
mobile_view_distance: int = 2
|
||||
display: Display = Display.block
|
||||
hide_inactive_cursor: JsBool = JsBool.true
|
||||
hide_cursor_time: int = 5000
|
||||
# Appearance options from RevealJS
|
||||
background_color: Color = "black"
|
||||
reveal_version: str = "5.1.0"
|
||||
reveal_theme: RevealTheme = RevealTheme.black
|
||||
title: str = "Manim Slides"
|
||||
background_color: Color = Field(
|
||||
"black",
|
||||
description="Background color used in slides, not relevant if videos fill the whole area.",
|
||||
)
|
||||
reveal_version: str = Field("5.1.0", description="RevealJS version.")
|
||||
reveal_theme: RevealTheme = Field(
|
||||
RevealTheme.black, description="RevealJS version."
|
||||
)
|
||||
title: str = Field("Manim Slides", description="Presentation title.")
|
||||
# Pydantic options
|
||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||
|
||||
@ -385,35 +551,32 @@ class RevealJS(Converter):
|
||||
|
||||
return resources.files(templates).joinpath("revealjs.html").read_text()
|
||||
|
||||
def open(self, file: Path) -> bool:
|
||||
def open(self, file: Path) -> None:
|
||||
"""
|
||||
Open the HTML file inside a web browser.
|
||||
|
||||
:param path: The path to the HTML file.
|
||||
"""
|
||||
return webbrowser.open(file.absolute().as_uri())
|
||||
webbrowser.open(file.absolute().as_uri())
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
def convert_to(self, dest: Path) -> None: # noqa: C901
|
||||
"""
|
||||
Convert this configuration into a RevealJS HTML presentation, saved to
|
||||
DEST.
|
||||
"""
|
||||
if self.data_uri:
|
||||
assets_dir = Path("") # Actually we won't care.
|
||||
else:
|
||||
dirname = dest.parent
|
||||
basename = dest.stem
|
||||
ext = dest.suffix
|
||||
dirname = dest.parent
|
||||
basename = dest.stem
|
||||
ext = dest.suffix
|
||||
|
||||
assets_dir = Path(
|
||||
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
||||
)
|
||||
full_assets_dir = dirname / assets_dir
|
||||
assets_dir = Path(
|
||||
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
|
||||
)
|
||||
full_assets_dir = dirname / assets_dir
|
||||
|
||||
if not self.one_file or self.offline:
|
||||
logger.debug(f"Assets will be saved to: {full_assets_dir}")
|
||||
|
||||
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not self.one_file:
|
||||
num_presentation_configs = len(self.presentation_configs)
|
||||
|
||||
if num_presentation_configs > 1:
|
||||
@ -432,6 +595,7 @@ class RevealJS(Converter):
|
||||
def prefix(i: int) -> str:
|
||||
return ""
|
||||
|
||||
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
for i, presentation_config in enumerate(self.presentation_configs):
|
||||
presentation_config.copy_to(
|
||||
full_assets_dir, include_reversed=False, prefix=prefix(i)
|
||||
@ -440,10 +604,14 @@ class RevealJS(Converter):
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(dest, "w") as f:
|
||||
revealjs_template = Template(self.load_template())
|
||||
revealjs_template = Template(
|
||||
self.load_template(), trim_blocks=True, lstrip_blocks=True
|
||||
)
|
||||
|
||||
options = self.dict()
|
||||
options["assets_dir"] = assets_dir
|
||||
options = self.model_dump()
|
||||
|
||||
if assets_dir is not None:
|
||||
options["assets_dir"] = assets_dir
|
||||
|
||||
has_notes = any(
|
||||
slide_config.notes != ""
|
||||
@ -456,25 +624,89 @@ class RevealJS(Converter):
|
||||
get_duration_ms=get_duration_ms,
|
||||
has_notes=has_notes,
|
||||
env=os.environ,
|
||||
prefix=prefix if not self.one_file else None,
|
||||
**options,
|
||||
)
|
||||
# If not offline, write the content to the file
|
||||
if not self.offline:
|
||||
f.write(content)
|
||||
return
|
||||
|
||||
# If offline, download remote assets and store them in the assets folder
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
session = requests.Session()
|
||||
|
||||
for tag, inner in [("link", "href"), ("script", "src")]:
|
||||
for item in soup.find_all(tag):
|
||||
if item.has_attr(inner) and (link := item[inner]).startswith(
|
||||
"http"
|
||||
):
|
||||
asset_name = link.rsplit("/", 1)[1]
|
||||
asset = session.get(link)
|
||||
if self.one_file:
|
||||
# If it is a CSS file, inline it
|
||||
if tag == "link" and "stylesheet" in item["rel"]:
|
||||
item.decompose()
|
||||
style = soup.new_tag("style")
|
||||
style.string = asset.text
|
||||
soup.head.append(style)
|
||||
# If it is a JS file, inline it
|
||||
elif tag == "script":
|
||||
item.decompose()
|
||||
script = soup.new_tag("script")
|
||||
script.string = asset.text
|
||||
soup.head.append(script)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unable to inline {tag} asset: {link}"
|
||||
)
|
||||
else:
|
||||
full_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
with open(full_assets_dir / asset_name, "wb") as asset_file:
|
||||
asset_file.write(asset.content)
|
||||
|
||||
item[inner] = str(assets_dir / asset_name)
|
||||
|
||||
content = str(soup)
|
||||
f.write(content)
|
||||
|
||||
|
||||
class HtmlZip(RevealJS):
|
||||
def open(self, file: Path) -> None:
|
||||
super(RevealJS, self).open(file) # Override opening with web browser
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""
|
||||
Convert this configuration into a zipped RevealJS HTML presentation, saved to
|
||||
DEST.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as directory_name:
|
||||
directory = Path(directory_name)
|
||||
|
||||
html_file = directory / dest.with_suffix(".html").name
|
||||
|
||||
super().convert_to(html_file)
|
||||
shutil.make_archive(str(dest.with_suffix("")), "zip", directory_name)
|
||||
|
||||
|
||||
class FrameIndex(str, Enum):
|
||||
first = "first"
|
||||
last = "last"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class PDF(Converter):
|
||||
frame_index: FrameIndex = FrameIndex.last
|
||||
resolution: PositiveFloat = 100.0
|
||||
frame_index: FrameIndex = Field(
|
||||
FrameIndex.last,
|
||||
description="What frame (first or last) is used to represent each slide.",
|
||||
)
|
||||
resolution: PositiveFloat = Field(
|
||||
100.0, description="Image resolution use for saving frames."
|
||||
)
|
||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||
|
||||
def open(self, file: Path) -> None:
|
||||
return open_with_default(file)
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""Convert this configuration into a PDF presentation, saved to DEST."""
|
||||
images = []
|
||||
@ -501,17 +733,30 @@ class PDF(Converter):
|
||||
|
||||
|
||||
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
|
||||
left: PositiveInt = Field(
|
||||
0, description="Horizontal offset where the video is placed from left border."
|
||||
)
|
||||
top: PositiveInt = Field(
|
||||
0, description="Vertical offset where the video is placed from top border."
|
||||
)
|
||||
width: PositiveInt = Field(
|
||||
1280,
|
||||
description="Width of the slides.\nThis should match the resolution of the presentation.",
|
||||
)
|
||||
height: PositiveInt = Field(
|
||||
720,
|
||||
description="Height of the slides.\nThis should match the resolution of the presentation.",
|
||||
)
|
||||
auto_play_media: bool = Field(
|
||||
True, description="Automatically play animations when changing slide."
|
||||
)
|
||||
poster_frame_image: Optional[FilePath] = Field(
|
||||
None,
|
||||
description="Optional image to use when animations are not playing.\n"
|
||||
"By default, the first frame of each animation is used.\nThis is important to avoid blinking effects between slides.",
|
||||
)
|
||||
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
||||
|
||||
def open(self, file: Path) -> None:
|
||||
return open_with_default(file)
|
||||
|
||||
def convert_to(self, dest: Path) -> None:
|
||||
"""Convert this configuration into a PowerPoint presentation, saved to DEST."""
|
||||
prs = pptx.Presentation()
|
||||
@ -586,22 +831,39 @@ class PowerPoint(Converter):
|
||||
|
||||
|
||||
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Wrap a function to add a `--show-config` option."""
|
||||
"""Wrap a function to add a '--show-config' option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
def callback(ctx: Context, _param: Parameter, value: bool) -> None:
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
|
||||
to = ctx.params.get("to", "html")
|
||||
if "to" in ctx.params:
|
||||
to = ctx.params["to"]
|
||||
cls = Converter.from_string(to)
|
||||
elif "dest" in ctx.params:
|
||||
dest = Path(ctx.params["dest"])
|
||||
fmt = dest.suffix[1:].lower()
|
||||
try:
|
||||
cls = Converter.from_string(fmt)
|
||||
except KeyError:
|
||||
logger.warning(
|
||||
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
|
||||
)
|
||||
cls = RevealJS
|
||||
else:
|
||||
cls = RevealJS
|
||||
|
||||
converter = Converter.from_string(to)
|
||||
if doc := getattr(cls, "__doc__", ""):
|
||||
click.echo(textwrap.dedent(doc))
|
||||
|
||||
for key, field in converter.model_fields.items():
|
||||
for key, field in cls.model_fields.items():
|
||||
if field.is_required():
|
||||
continue
|
||||
|
||||
default = field.get_default(call_default_factory=True)
|
||||
click.echo(f"{key}: {default}")
|
||||
click.echo(click.style(key, bold=True) + f": {default}")
|
||||
if description := field.description:
|
||||
click.secho(textwrap.indent(description, prefix="# "), dim=True)
|
||||
|
||||
ctx.exit()
|
||||
|
||||
@ -617,18 +879,31 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
|
||||
|
||||
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Wrap a function to add a `--show-template` option."""
|
||||
"""Wrap a function to add a '--show-template' option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
|
||||
to = ctx.params.get("to", "html")
|
||||
if "to" in ctx.params:
|
||||
to = ctx.params["to"]
|
||||
cls = Converter.from_string(to)
|
||||
elif "dest" in ctx.params:
|
||||
dest = Path(ctx.params["dest"])
|
||||
fmt = dest.suffix[1:].lower()
|
||||
try:
|
||||
cls = Converter.from_string(fmt)
|
||||
except KeyError:
|
||||
logger.warning(
|
||||
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
|
||||
)
|
||||
cls = RevealJS
|
||||
else:
|
||||
cls = RevealJS
|
||||
|
||||
template = ctx.params.get("template", None)
|
||||
|
||||
converter = Converter.from_string(to)(
|
||||
presentation_configs=[PresentationConfig()], template=template
|
||||
)
|
||||
converter = cls(presentation_configs=[], template=template)
|
||||
click.echo(converter.load_template())
|
||||
|
||||
ctx.exit()
|
||||
@ -650,7 +925,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
|
||||
@click.option(
|
||||
"--to",
|
||||
type=click.Choice(["auto", "html", "pdf", "pptx"], case_sensitive=False),
|
||||
type=click.Choice(["auto", "html", "pdf", "pptx", "zip"], case_sensitive=False),
|
||||
metavar="FORMAT",
|
||||
default="auto",
|
||||
show_default=True,
|
||||
@ -662,7 +937,6 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
is_flag=True,
|
||||
help="Open the newly created file using the appropriate application.",
|
||||
)
|
||||
@click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.")
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
@ -670,7 +944,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
multiple=True,
|
||||
callback=validate_config_option,
|
||||
help="Configuration options passed to the converter. "
|
||||
"E.g., pass ``-cslide_number=true`` to display slide numbers.",
|
||||
"E.g., pass '-cslide_number=true' to display slide numbers.",
|
||||
)
|
||||
@click.option(
|
||||
"--use-template",
|
||||
@ -678,7 +952,19 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
|
||||
metavar="FILE",
|
||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||
help="Use the template given by FILE instead of default one. "
|
||||
"To echo the default template, use ``--show-template``.",
|
||||
"To echo the default template, use '--show-template'.",
|
||||
)
|
||||
@click.option(
|
||||
"--one-file",
|
||||
is_flag=True,
|
||||
help="Embed all local assets (e.g., video files) in the output file. "
|
||||
"The is a convenient alias to '-cone_file=true'.",
|
||||
)
|
||||
@click.option(
|
||||
"--offline",
|
||||
is_flag=True,
|
||||
help="Download any remote content and store it in the assets folder. "
|
||||
"The is a convenient alias to '-coffline=true'.",
|
||||
)
|
||||
@show_template_option
|
||||
@show_config_options
|
||||
@ -689,9 +975,10 @@ def convert(
|
||||
dest: Path,
|
||||
to: str,
|
||||
open_result: bool,
|
||||
force: bool,
|
||||
config_options: dict[str, str],
|
||||
template: Optional[Path],
|
||||
offline: bool,
|
||||
one_file: bool,
|
||||
) -> None:
|
||||
"""Convert SCENE(s) into a given format and writes the result in DEST."""
|
||||
presentation_configs = [PresentationConfig.from_file(scene) for scene in scenes]
|
||||
@ -702,13 +989,42 @@ def convert(
|
||||
try:
|
||||
cls = Converter.from_string(fmt)
|
||||
except KeyError:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
|
||||
)
|
||||
cls = RevealJS
|
||||
else:
|
||||
cls = Converter.from_string(to)
|
||||
|
||||
if (
|
||||
one_file
|
||||
and issubclass(cls, (RevealJS, HtmlZip))
|
||||
and "one_file" not in config_options
|
||||
):
|
||||
config_options["one_file"] = "true"
|
||||
|
||||
# Change data_uri to one_file and print a warning if present
|
||||
if "data_uri" in config_options:
|
||||
warnings.warn(
|
||||
"The 'data_uri' configuration option is deprecated and will be "
|
||||
"removed in the next major version. "
|
||||
"Use 'one_file' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
config_options["one_file"] = (
|
||||
config_options["one_file"]
|
||||
if "one_file" in config_options
|
||||
else config_options.pop("data_uri")
|
||||
)
|
||||
|
||||
if (
|
||||
offline
|
||||
and issubclass(cls, (RevealJS, HtmlZip))
|
||||
and "offline" not in config_options
|
||||
):
|
||||
config_options["offline"] = "true"
|
||||
|
||||
converter = cls(
|
||||
presentation_configs=presentation_configs,
|
||||
template=template,
|
||||
|
Reference in New Issue
Block a user