[Python] Typst in Manim. I make Manim to support for the Typst typesetting language

Discussão em 'Python' iniciado por Stack, Junho 10, 2025.

  1. Stack

    Stack Membro Participativo

    I make Manim to support for the Typst typesetting language but It seem like horizontal line in between numerator and denominator such as (z^2)/(w^2) is not appear. Seem Demo(scene).

    from manim import *
    import os
    import shutil
    import subprocess

    # Define a temporary directory for Typst output
    TYPST_TEMP_DIR = "typst_temp_files"

    def str_to_valid_filename(s: str) -> str:
    """Converts a string to a valid filename."""
    return "".join((c for c in s if c.isalnum() or c in "._-")).strip()

    def common_str_to_typst_str(common_str: str, font_size_pt: int = 50, color_hex: str = "#FFFFFF") -> str:
    """
    Generates a Typst string with specified font size and color.
    The font_size_pt here is the actual font size Typst will render at.
    """
    # Ensure background is transparent and text color/size are set
    template = rf"""#set page(fill: none)
    #set text(fill: rgb("{color_hex}"), size: {font_size_pt}pt)
    // Set page width to auto to prevent excessive padding, allowing Typst to fit content
    #set page(width: auto, height: auto)
    {common_str}"""
    return template

    def math_str_to_common_str(math_str: str) -> str:
    """Wraps a math string in Typst's inline math delimiters."""
    return f"$ {math_str} $"

    def typst_str_to_typ(typst_str: str, typst_file_name: str, folder: str = TYPST_TEMP_DIR) -> str:
    """Writes the Typst content to a .typ file."""
    os.makedirs(folder, exist_ok=True) # Use exist_ok=True to avoid error if folder exists
    file_path = os.path.join(folder, typst_file_name)
    with open(file_path, "w", encoding="utf-8") as f:
    f.write(typst_str)
    return file_path

    def typ_to_svg(typ_file_path: str) -> str:
    """Compiles a .typ file to an SVG file using the Typst CLI."""
    svg_file_path = typ_file_path.replace(".typ", ".svg")

    if os.path.exists(svg_file_path):
    return svg_file_path # Return existing SVG for caching, avoiding re-compilation

    command = [
    'typst', 'compile',
    '--format', 'svg',
    typ_file_path,
    svg_file_path
    ]

    try:
    # Use subprocess.run for better error handling and simplicity
    result = subprocess.run(command, check=True, capture_output=True, text=True, encoding="utf-8")
    if result.stdout:
    print(f"Typst stdout: {result.stdout}")
    if result.stderr:
    print(f"Typst stderr: {result.stderr}")
    except subprocess.CalledProcessError as e:
    print(f"Error compiling Typst file: {typ_file_path}")
    print(f"Typst stderr: {e.stderr}")
    raise RuntimeError(f"Typst compilation failed: {e.stderr}") from e
    except FileNotFoundError:
    raise RuntimeError("Typst command not found. Please ensure Typst is installed and in your system's PATH.")

    if not os.path.exists(svg_file_path):
    raise RuntimeError(f"SVG file not generated at expected path: {svg_file_path}")

    return svg_file_path

    def common_str_to_svg(common_str: str, font_size_pt: int = 50, color_hex: str = "#FFFFFF", folder: str = TYPST_TEMP_DIR) -> str:
    """
    Converts a common string to an SVG file path by rendering it with Typst.
    This function will generate temporary .typ and .svg files.
    """
    # Create a unique filename for the Typst file to avoid conflicts
    unique_filename = f"{str_to_valid_filename(common_str)}_{hash(common_str) % 100000}.typ"

    typst_str = common_str_to_typst_str(common_str, font_size_pt, color_hex)
    typ_file_path = typst_str_to_typ(typst_str, unique_filename, folder)
    svg_file_path = typ_to_svg(typ_file_path)
    return svg_file_path

    class Typst(SVGMobject):
    """
    A Manim Mobject for rendering Typst content.

    Parameters
    ----------
    common_str
    The string content to be rendered by Typst.
    font_size
    The desired visual font size in Manim units (similar to Text/MathTex).
    color
    The color of the text, as a ManimColor or hex string.
    folder
    The directory to store temporary Typst files.
    kwargs
    Additional keyword arguments passed to SVGMobject.
    """
    def __init__(self, common_str: str, font_size: float = 0.7, color: ManimColor | str = WHITE, folder: str = TYPST_TEMP_DIR, **kwargs):
    TYPST_RENDER_FONT_SIZE_PT = 200 # Render Typst at a large, fixed point size for quality

    # Ensure color is a hex string for Typst
    if isinstance(color, ManimColor):
    color_hex = color.to_hex() # <-- FIX IS HERE: changed .hex to .to_hex()
    else:
    color_hex = color

    self.svg_file_path = common_str_to_svg(common_str, TYPST_RENDER_FONT_SIZE_PT, color_hex, folder)

    self._common_str = common_str
    self._font_size = font_size
    self._color = color
    self._folder = folder
    self._kwargs = kwargs

    if "stroke_width" not in kwargs:
    kwargs["stroke_width"] = 0

    super().__init__(self.svg_file_path, **kwargs)

    if self.height > 1e-9:
    self.scale(font_size / self.height)
    else:
    self.set_height(font_size)

    self.set_color(self._color)

    @property
    def font_size(self) -> float:
    return self._font_size

    @font_size.setter
    def font_size(self, new_font_size: float):
    if new_font_size <= 0:
    raise ValueError("Font size must be greater than 0.")
    self.set_height(new_font_size)
    self._font_size = new_font_size

    def set_content(self, common_str: str, font_size: float = None, color: ManimColor | str = None, folder: str = None):
    """
    Updates the content of the Typst mobject.
    Removes the old SVG file and generates a new one.
    """
    if os.path.exists(self.svg_file_path):
    os.remove(self.svg_file_path)

    current_font_size = font_size if font_size is not None else self._font_size
    current_color = color if color is not None else self._color
    current_folder = folder if folder is not None else self._folder

    if isinstance(current_color, ManimColor):
    color_hex = current_color.to_hex() # <-- FIX IS HERE: changed .hex to .to_hex()
    else:
    color_hex = current_color

    TYPST_RENDER_FONT_SIZE_PT = 200
    new_svg_file_path = common_str_to_svg(common_str, TYPST_RENDER_FONT_SIZE_PT, color_hex, current_folder)

    temp_kwargs = self._kwargs.copy()
    new_mob = SVGMobject(new_svg_file_path, **temp_kwargs)

    if new_mob.height > 1e-9:
    new_mob.scale(current_font_size / new_mob.height)
    else:
    new_mob.set_height(current_font_size)

    new_mob.set_color(current_color)

    self.become(new_mob)
    self.svg_file_path = new_svg_file_path

    self._common_str = common_str
    self._font_size = current_font_size
    self._color = current_color
    self._folder = current_folder

    return self

    class MathTypst(Typst):
    """
    A Manim Mobject for rendering Typst mathematical content.

    Parameters
    ----------
    math_str
    The mathematical string content to be rendered by Typst.
    font_size
    The desired visual font size in Manim units.
    color
    The color of the text, as a ManimColor or hex string.
    folder
    The directory to store temporary Typst files.
    kwargs
    Additional keyword arguments passed to SVGMobject.
    """
    def __init__(self, math_str: str, font_size: float = 0.7, color: ManimColor | str = WHITE, folder: str = TYPST_TEMP_DIR, **kwargs):
    super().__init__(math_str_to_common_str(math_str), font_size, color, folder, **kwargs)

    def set_content(self, math_str: str, font_size: float = None, color: ManimColor | str = None, folder: str = None):
    """
    Updates the content of the MathTypst mobject with new math string.
    """
    super().set_content(math_str_to_common_str(math_str), font_size, color, folder)
    return self

    # You might want to add a cleanup function for temporary files
    def cleanup_typst_temp_files(folder: str = TYPST_TEMP_DIR):
    """Removes the temporary Typst files folder."""
    if os.path.exists(folder):
    print(f"Cleaning up temporary Typst files in: {folder}")
    shutil.rmtree(folder)


    # Clean up
    # cleanup_typst_temp_files()



    class Demo(Scene):
    def construct(self):
    tex = Typst("Hello Typst!", color=YELLOW)
    math = Typst(r"$x^2 + y^2 = (z^2)/(w^2)$", color=BLUE)
    mathlatex = Tex(r"$x^2+y^2 = \frac{z2}{w^2}$")

    group = VGroup(tex, math, mathlatax).arrange(DOWN, buff=1)
    self.play(Write(group))

    # Clean up
    cleanup_typst_temp_files()

    Continue reading...

Compartilhe esta Página