├── python └── typst │ ├── py.typed │ ├── __init__.py │ └── __init__.pyi ├── .github ├── dependabot.yml └── workflows │ └── CI.yml ├── src ├── download.rs ├── query.rs ├── compiler.rs ├── world.rs └── lib.rs ├── pyproject.toml ├── .gitignore ├── Cargo.toml ├── README.md ├── tests ├── hello.typ └── test_typst.py ├── LICENSE └── uv.lock /python/typst/py.typed: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /python/typst/__init__.py: -------------------------------------------------------------------------------- 1 | from ._typst import * 2 | 3 | 4 | __doc__ = _typst.__doc__ 5 | __all__ = _typst.__all__ 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /src/download.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use typst_kit::download::{DownloadState, Downloader, Progress}; 4 | 5 | pub struct SlientDownload(pub T); 6 | 7 | impl Progress for SlientDownload { 8 | fn print_start(&mut self) {} 9 | 10 | fn print_progress(&mut self, _state: &DownloadState) {} 11 | 12 | fn print_finish(&mut self, _state: &DownloadState) {} 13 | } 14 | 15 | /// Returns a new downloader. 16 | pub fn downloader() -> Downloader { 17 | let user_agent = concat!("typst-py/", env!("CARGO_PKG_VERSION")); 18 | Downloader::new(user_agent) 19 | } 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.0,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "typst" 7 | requires-python = ">=3.8" 8 | classifiers = [ 9 | "Programming Language :: Rust", 10 | "Programming Language :: Python :: Implementation :: CPython", 11 | "Programming Language :: Python :: Implementation :: PyPy", 12 | ] 13 | dynamic = ["version"] 14 | dependencies = [] 15 | license = "Apache-2.0" 16 | license-files = ["LICENSE"] 17 | readme = "README.md" 18 | 19 | [project.urls] 20 | 21 | Homepage = "https://github.com/messense/typst-py/" 22 | Repository = "https://github.com/messense/typst-py" 23 | Readme = "https://github.com/messense/typst-py/blob/main/README.md" 24 | Documentation = "https://github.com/messense/typst-py/blob/main/README.md" 25 | 26 | [dependency-groups] 27 | dev = [ 28 | "pytest>=8.3.5", 29 | ] 30 | 31 | [tool.maturin] 32 | module-name = "typst._typst" 33 | python-source = "python" 34 | features = ["pyo3/extension-module"] 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | .pytest_cache/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | .venv/ 14 | env/ 15 | bin/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | include/ 26 | man/ 27 | venv/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | pip-selfcheck.json 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | 60 | .DS_Store 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyCharm 66 | .idea/ 67 | 68 | # VSCode 69 | .vscode/ 70 | 71 | # Pyenv 72 | .python-version 73 | 74 | *.pdf -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "typst-py" 3 | version = "0.14.4" 4 | edition = "2024" 5 | description = "Python binding to typst" 6 | license = "Apache-2.0" 7 | repository = "https://github.com/messense/typst-py" 8 | readme = "README.md" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | [lib] 12 | crate-type = ["cdylib"] 13 | 14 | [dependencies] 15 | chrono = { version = "0.4.42", default-features = false, features = [ 16 | "clock", 17 | "std", 18 | ] } 19 | codespan-reporting = "0.13" 20 | comemo = "0.5.0" 21 | ecow = "0.2" 22 | pathdiff = "0.2" 23 | pyo3 = { version = "0.27.1", features = ["abi3-py38", "generate-import-lib"] } 24 | serde = { version = "1.0.228", features = ["derive"] } 25 | serde_json = "1" 26 | serde_yaml = "0.9" 27 | typst = "0.14.1" 28 | typst-kit = { version = "0.14.1", features = [ 29 | "downloads", 30 | "embed-fonts", 31 | "vendor-openssl", 32 | ] } 33 | typst-pdf = "0.14.1" 34 | typst-svg = "0.14.1" 35 | typst-render = "0.14.1" 36 | typst-eval = "0.14.1" 37 | typst-html = "0.14.1" 38 | rustc-hash = "2.1.1" 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typst-py 2 | 3 | ![CI](https://github.com/messense/typst-py/workflows/CI/badge.svg) 4 | [![PyPI](https://img.shields.io/pypi/v/typst.svg)](https://pypi.org/project/typst) 5 | 6 | Python binding to [typst](https://github.com/typst/typst), 7 | a new markup-based typesetting system that is powerful and easy to learn. 8 | 9 | ## Installation 10 | 11 | ```bash 12 | pip install typst 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```python 18 | import typst 19 | 20 | 21 | # Compile `hello.typ` to PDF and save as `hello.pdf` 22 | typst.compile("hello.typ", output="hello.pdf") 23 | 24 | # Compile `hello.typ` to PNG and save as `hello.png` 25 | typst.compile("hello.typ", output="hello.png", format="png", ppi=144.0) 26 | 27 | # Or pass `hello.typ` content as bytes 28 | with open("hello.typ", "rb") as f: 29 | typst.compile(f.read(), output="hello.pdf") 30 | 31 | # Or return PDF content as bytes 32 | pdf_bytes = typst.compile("hello.typ") 33 | 34 | # Also for svg 35 | svg_bytes = typst.compile("hello.typ", format="svg") 36 | 37 | # For multi-page export (the template is the same as the typst cli) 38 | images = typst.compile("hello.typ", output="hello{n}.png", format="png") 39 | 40 | # Or use Compiler class to avoid reinitialization 41 | compiler = typst.Compiler() 42 | compiler.compile(input="hello.typ", format="png", ppi=144.0) 43 | 44 | # Query something 45 | import json 46 | 47 | values = json.loads(typst.query("hello.typ", "", field="value", one=True)) 48 | ``` 49 | 50 | ## Passing values 51 | 52 | You can pass values to the compiled Typst file with the `sys_inputs` argument. For example: 53 | 54 | ```python 55 | import json 56 | import typst 57 | 58 | persons = [{"name": "John", "age": 35}, {"name": "Xoliswa", "age": 45}] 59 | sys_inputs = {"persons": json.dumps(persons)} 60 | 61 | typst.compile(input="main.typ", output="ages.pdf", sys_inputs=sys_inputs) 62 | ``` 63 | 64 | The following example shows how the passed data can be used in a Typst file. 65 | 66 | ``` 67 | #let persons = json(bytes(sys.inputs.persons)) 68 | 69 | #for person in persons [ 70 | #person.name is #person.age years old. \ 71 | ] 72 | ``` 73 | 74 | ## License 75 | 76 | This work is released under the Apache-2.0 license. A copy of the license is provided in the [LICENSE](./LICENSE) file. 77 | -------------------------------------------------------------------------------- /tests/hello.typ: -------------------------------------------------------------------------------- 1 | #set document( 2 | title: "Hello Typst", 3 | author: "Juno Takano", 4 | date: auto, 5 | keywords: ("typst", "typesetting"), 6 | ) 7 | #set page(paper: "a6", margin: (x: 0.8cm, y: 1cm), fill: rgb("#fffddf")) 8 | #set heading(numbering: "1.") 9 | #set par(justify: true, leading: 0.7em,) 10 | #set quote(block: true, quotes: true) 11 | #set footnote.entry(gap: 1em, clearance: 1em) 12 | #show link: underline 13 | 14 | = Typst 15 | Typst is a typesetting system that takes code in and outputs PDFs. 16 | 17 | This file is an example of several features you can use in it. 18 | 19 | == Math notation 20 | The first example Typst shows you is for writing the 21 | Fibonacci sequence's definition through its 22 | recurrence relation $F_n = F_(n-1) + F_(n-2)$. That's inline math for you. 23 | 24 | You can also do math on its own, centered paragraph: 25 | 26 | $ F_n = round(1 / sqrt(5) phi.alt^n), quad 27 | phi.alt = (1 + sqrt(5)) / 2 $ 28 | 29 | == Code blocks 30 | Typst also supports code blocks. The code for the previous formula, for instance, was: 31 | 32 | #block( fill: luma(230), inset: 8pt, radius: 4pt, breakable: false)[ 33 | ```typst 34 | $ F_n = round(1 / sqrt(5) phi.alt^n), quad 35 | phi.alt = (1 + sqrt(5)) / 2 $ 36 | ```] 37 | 38 | == Code mode 39 | You can define and use code logic for Typst to evaluate on compile: 40 | 41 | #block( fill: luma(230), inset: 8pt, radius: 4pt, breakable: false)[ 42 | ```typst 43 | #let count = 8 44 | #let nums = range(1, count + 1) 45 | #let fib(n) = ( 46 | if n <= 2 { 1 } 47 | else { fib(n - 1) + fib(n - 2) } 48 | ) 49 | ```] 50 | 51 | #let count = 8 52 | #let nums = range(1, count + 1) 53 | #let fib(n) = ( 54 | if n <= 2 { 1 } 55 | else { fib(n - 1) + fib(n - 2) } 56 | ) 57 | 58 | Using the `#count` and `#nums` values just set, we can render the following table: 59 | 60 | #align(center, table( 61 | columns: count, 62 | ..nums.map(n => $F_#n$), 63 | ..nums.map(n => str(fib(n))), 64 | )) 65 | 66 | == Formatting 67 | This *bold text* is created using `*asterisks*`. _Italics_ are made using `_underlines_`. 68 | 69 | - An unordered list 70 | - with a few 71 | - items uses hyphens 72 | - for markers 73 | 74 | + This numbered list 75 | + uses instead 76 | + the ```typst +``` sign 77 | + for each item 78 | 79 | #pagebreak() 80 | 81 | == Quotes 82 | There is also a `#quote` function: 83 | 84 | #quote(attribution: [#link("https://typst.app/docs/tutorial/writing-in-typst/")[Typst Docs, _Writing in typst_]])[ 85 | The caption consists of arbitrary markup. To give markup to 86 | a function, we enclose it in square brackets. This construct 87 | is called a content block. 88 | ] 89 | 90 | == Footnotes 91 | Speaking of quotes, footnotes append linked references at the end of the document. 92 | #footnote[ 93 | #" "#link("https://typst.app/docs/reference/meta/footnote/")[Typst reference, _footnote_] 94 | ] 95 | 96 | You can use ```typst #" "``` or #link("https://typst.app/docs/reference/layout/h/")[horizontal spacing] to adjust the distance between the superscript number and the text. 97 | #footnote([#" "Though I'd rather use a parameter in `set footnote.entry()`. Also, they are a bit hard to click.]) 98 | 99 | They can also be labeled so you may reference them multiple times. This line uses the same reference as the first footnote. 100 | #footnote() 101 | 102 | == A math lorem 103 | $ 1.62 theta + 104 | sum_(i=0)^nabla R_n / "10p" arrow 105 | p := vec(x_1, y_2, z_3) arrow " ?" $ 106 | -------------------------------------------------------------------------------- /src/query.rs: -------------------------------------------------------------------------------- 1 | use comemo::Track; 2 | use ecow::{EcoString, eco_format}; 3 | use serde::Serialize; 4 | use typst::World; 5 | use typst::diag::{StrResult, Warned, bail}; 6 | use typst::engine::Sink; 7 | use typst::foundations::{Content, IntoValue, LocatableSelector, Scope}; 8 | use typst::layout::PagedDocument; 9 | use typst::syntax::Span; 10 | use typst::syntax::SyntaxMode; 11 | use typst_eval::eval_string; 12 | 13 | use crate::world::SystemWorld; 14 | 15 | /// Processes an input file to extract provided metadata 16 | #[derive(Debug, Clone)] 17 | pub struct QueryCommand { 18 | /// Defines which elements to retrieve 19 | pub selector: String, 20 | 21 | /// Extracts just one field from all retrieved elements 22 | pub field: Option, 23 | 24 | /// Expects and retrieves exactly one element 25 | pub one: bool, 26 | 27 | /// The format to serialize in 28 | pub format: SerializationFormat, 29 | } 30 | 31 | // Output file format for query command 32 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 33 | pub enum SerializationFormat { 34 | Json, 35 | Yaml, 36 | } 37 | 38 | /// Execute a query command. 39 | pub fn query(world: &mut SystemWorld, command: &QueryCommand) -> StrResult { 40 | let Warned { output, warnings } = typst::compile(world); 41 | 42 | match output { 43 | // Retrieve and print query results. 44 | Ok(document) => { 45 | let data = retrieve(world, command, &document)?; 46 | let serialized = format(data, command)?; 47 | Ok(serialized) 48 | } 49 | // Print errors and warnings. 50 | Err(errors) => { 51 | let mut message = EcoString::from("failed to compile document"); 52 | for (i, error) in errors.into_iter().enumerate() { 53 | message.push_str(if i == 0 { ": " } else { ", " }); 54 | message.push_str(&error.message); 55 | } 56 | for warning in warnings { 57 | message.push_str(": "); 58 | message.push_str(&warning.message); 59 | } 60 | Err(message) 61 | } 62 | } 63 | } 64 | 65 | /// Retrieve the matches for the selector. 66 | fn retrieve( 67 | world: &dyn World, 68 | command: &QueryCommand, 69 | document: &PagedDocument, 70 | ) -> StrResult> { 71 | let selector = eval_string( 72 | &typst::ROUTINES, 73 | world.track(), 74 | Sink::new().track_mut(), 75 | &command.selector, 76 | Span::detached(), 77 | SyntaxMode::Code, 78 | Scope::default(), 79 | ) 80 | .map_err(|errors| { 81 | let mut message = EcoString::from("failed to evaluate selector"); 82 | for (i, error) in errors.into_iter().enumerate() { 83 | message.push_str(if i == 0 { ": " } else { ", " }); 84 | message.push_str(&error.message); 85 | } 86 | message 87 | })? 88 | .cast::() 89 | .map_err(|e| e.message().clone())?; 90 | 91 | Ok(document 92 | .introspector 93 | .query(&selector.0) 94 | .into_iter() 95 | .collect::>()) 96 | } 97 | 98 | /// Format the query result in the output format. 99 | fn format(elements: Vec, command: &QueryCommand) -> StrResult { 100 | if command.one && elements.len() != 1 { 101 | bail!("expected exactly one element, found {}", elements.len()); 102 | } 103 | 104 | let mapped: Vec<_> = elements 105 | .into_iter() 106 | .filter_map(|c| match &command.field { 107 | Some(field) => c.get_by_name(field).ok(), 108 | _ => Some(c.into_value()), 109 | }) 110 | .collect(); 111 | 112 | if command.one { 113 | let Some(value) = mapped.first() else { 114 | bail!("no such field found for element"); 115 | }; 116 | serialize(value, command.format) 117 | } else { 118 | serialize(&mapped, command.format) 119 | } 120 | } 121 | 122 | /// Serialize data to the output format. 123 | fn serialize(data: &impl Serialize, format: SerializationFormat) -> StrResult { 124 | match format { 125 | SerializationFormat::Json => { 126 | serde_json::to_string_pretty(data).map_err(|e| eco_format!("{e}")) 127 | } 128 | SerializationFormat::Yaml => serde_yaml::to_string(&data).map_err(|e| eco_format!("{e}")), 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by maturin v0.14.16 2 | # To update, run 3 | # 4 | # maturin generate-ci github 5 | # 6 | name: CI 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | tags: 14 | - "*" 15 | pull_request: 16 | workflow_dispatch: 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number }} 20 | cancel-in-progress: true 21 | 22 | permissions: 23 | contents: read 24 | 25 | jobs: 26 | linux: 27 | runs-on: ${{ matrix.platform.runner || 'ubuntu-latest' }} 28 | strategy: 29 | matrix: 30 | platform: 31 | - target: x86_64 32 | - target: aarch64 33 | runner: ubuntu-24.04-arm 34 | - target: armv7 35 | - target: s390x 36 | - target: ppc64le 37 | steps: 38 | - uses: actions/checkout@v3 39 | - uses: actions/setup-python@v4 40 | with: 41 | python-version: "3.10" 42 | - name: Build wheels 43 | uses: PyO3/maturin-action@v1 44 | env: 45 | # Make psm compile, see https://github.com/rust-lang/stacker/issues/79 46 | CFLAGS_s390x_unknown_linux_gnu: "-march=z10" 47 | with: 48 | target: ${{ matrix.platform.target }} 49 | args: --release --out dist 50 | sccache: "true" 51 | manylinux: ${{ matrix.platform.manylinux || 'auto' }} 52 | before-script-linux: | 53 | which yum > /dev/null && yum install -y perl-core 54 | - name: Build free-threaded wheels 55 | uses: PyO3/maturin-action@v1 56 | env: 57 | # Make psm compile, see https://github.com/rust-lang/stacker/issues/79 58 | CFLAGS_s390x_unknown_linux_gnu: "-march=z10" 59 | with: 60 | target: ${{ matrix.platform.target }} 61 | args: --release --out dist -i python3.13t 62 | sccache: "true" 63 | manylinux: ${{ matrix.platform.manylinux || 'auto' }} 64 | before-script-linux: | 65 | which yum > /dev/null && yum install -y perl-IPC-Cmd 66 | - name: Upload wheels 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: wheels-linux-${{ matrix.platform.target }} 70 | path: dist 71 | 72 | windows: 73 | runs-on: windows-latest 74 | strategy: 75 | matrix: 76 | target: [x64] 77 | steps: 78 | - uses: actions/checkout@v3 79 | - uses: actions/setup-python@v4 80 | with: 81 | python-version: "3.10" 82 | architecture: ${{ matrix.target }} 83 | - uses: dtolnay/rust-toolchain@stable 84 | - name: Build wheels 85 | uses: PyO3/maturin-action@v1 86 | with: 87 | target: ${{ matrix.target }} 88 | args: --release --out dist 89 | sccache: "true" 90 | - name: Build free-threaded wheels 91 | uses: PyO3/maturin-action@v1 92 | with: 93 | target: ${{ matrix.target }} 94 | args: --release --out dist -i python3.13t 95 | sccache: "true" 96 | - name: Upload wheels 97 | uses: actions/upload-artifact@v4 98 | with: 99 | name: wheels-windows-${{ matrix.target }} 100 | path: dist 101 | 102 | macos: 103 | runs-on: macos-latest 104 | strategy: 105 | matrix: 106 | target: [x86_64, aarch64] 107 | steps: 108 | - uses: actions/checkout@v3 109 | - uses: actions/setup-python@v4 110 | with: 111 | python-version: "3.10" 112 | - uses: dtolnay/rust-toolchain@stable 113 | - name: Build wheels 114 | uses: PyO3/maturin-action@v1 115 | with: 116 | target: ${{ matrix.target }} 117 | args: --release --out dist 118 | sccache: "true" 119 | - name: Build free-threaded wheels 120 | uses: PyO3/maturin-action@v1 121 | with: 122 | target: ${{ matrix.target }} 123 | args: --release --out dist -i python3.13t 124 | sccache: "true" 125 | - name: Upload wheels 126 | uses: actions/upload-artifact@v4 127 | with: 128 | name: wheels-macos-${{ matrix.target }} 129 | path: dist 130 | 131 | sdist: 132 | runs-on: ubuntu-latest 133 | steps: 134 | - uses: actions/checkout@v3 135 | - name: Build sdist 136 | uses: PyO3/maturin-action@v1 137 | with: 138 | command: sdist 139 | args: --out dist 140 | - name: Upload sdist 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: wheels-sdist 144 | path: dist 145 | 146 | release: 147 | permissions: 148 | # Used to upload to PyPI using trusted publisher. 149 | id-token: write 150 | name: Release 151 | runs-on: ubuntu-latest 152 | if: startsWith(github.ref, 'refs/tags/') 153 | needs: [linux, windows, macos, sdist] 154 | steps: 155 | - uses: actions/download-artifact@v4 156 | with: 157 | pattern: wheels-* 158 | merge-multiple: true 159 | - name: Publish to PyPI 160 | uses: PyO3/maturin-action@v1 161 | with: 162 | command: upload 163 | args: --skip-existing * 164 | -------------------------------------------------------------------------------- /src/compiler.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Datelike, Timelike}; 2 | use codespan_reporting::diagnostic::{Diagnostic, Label}; 3 | use codespan_reporting::term::{self, termcolor}; 4 | use ecow::eco_format; 5 | use typst::WorldExt; 6 | use typst::diag::{At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned}; 7 | use typst::foundations::Datetime; 8 | use typst::layout::PagedDocument; 9 | use typst::syntax::{FileId, Lines, Span}; 10 | use typst_html::HtmlDocument; 11 | 12 | use crate::world::SystemWorld; 13 | 14 | type CodespanResult = Result; 15 | type CodespanError = codespan_reporting::files::Error; 16 | 17 | type CompileSuccess = (Vec>, Vec); 18 | type CompileError = (Vec, Vec); 19 | 20 | impl SystemWorld { 21 | /// Compile and return structured diagnostics for error handling 22 | pub fn compile_with_diagnostics( 23 | &mut self, 24 | format: Option<&str>, 25 | ppi: Option, 26 | pdf_standards: &[typst_pdf::PdfStandard], 27 | ) -> Result { 28 | let normalized_format = format.unwrap_or("pdf").to_ascii_lowercase(); 29 | 30 | let Warned { output, warnings } = match normalized_format.as_str() { 31 | "html" => self.compile_and_export_html(), 32 | "pdf" | "png" | "svg" => { 33 | self.compile_and_export_paged(normalized_format.as_str(), ppi, pdf_standards) 34 | } 35 | _ => return Err((vec![], vec![])), 36 | }; 37 | 38 | match output { 39 | Ok(data) => Ok((data, warnings.to_vec())), 40 | Err(errors) => Err((errors.to_vec(), warnings.to_vec())), 41 | } 42 | } 43 | 44 | /// Compile and export paginated formats (PDF, PNG, SVG) - similar to compile_and_export in typst-cli 45 | fn compile_and_export_paged( 46 | &mut self, 47 | format: &str, 48 | ppi: Option, 49 | pdf_standards: &[typst_pdf::PdfStandard], 50 | ) -> Warned>>> { 51 | let Warned { output, warnings } = typst::compile::(self); 52 | // Evict comemo cache to limit memory usage after compilation 53 | comemo::evict(10); 54 | 55 | let result = output.and_then(|document| match format { 56 | "pdf" => { 57 | let standards = typst_pdf::PdfStandards::new(pdf_standards) 58 | .map_err(|e| eco_format!("PDF standards error: {:?}", e)) 59 | .at(Span::detached())?; 60 | export_pdf(&document, self, standards).map(|pdf| vec![pdf]) 61 | } 62 | "png" => export_image(&document, ImageExportFormat::Png, ppi).at(Span::detached()), 63 | "svg" => export_image(&document, ImageExportFormat::Svg, ppi).at(Span::detached()), 64 | _ => unreachable!(), 65 | }); 66 | 67 | Warned { 68 | output: result, 69 | warnings, 70 | } 71 | } 72 | 73 | /// Compile and export HTML format - similar to compile_and_export in typst-cli 74 | fn compile_and_export_html(&mut self) -> Warned>>> { 75 | let Warned { output, warnings } = typst::compile::(self); 76 | // Evict comemo cache to limit memory usage after compilation 77 | comemo::evict(10); 78 | 79 | let result = 80 | output.and_then(|document| export_html(&document, self).map(|html| vec![html])); 81 | 82 | Warned { 83 | output: result, 84 | warnings, 85 | } 86 | } 87 | } 88 | 89 | /// Export to a html. 90 | #[inline] 91 | fn export_html(document: &HtmlDocument, _world: &SystemWorld) -> SourceResult> { 92 | let buffer = typst_html::html(document)?; 93 | Ok(buffer.into()) 94 | } 95 | 96 | /// Export to a PDF. 97 | #[inline] 98 | fn export_pdf( 99 | document: &PagedDocument, 100 | _world: &SystemWorld, 101 | standards: typst_pdf::PdfStandards, 102 | ) -> SourceResult> { 103 | let buffer = typst_pdf::pdf( 104 | document, 105 | &typst_pdf::PdfOptions { 106 | ident: typst::foundations::Smart::Auto, 107 | timestamp: now().map(typst_pdf::Timestamp::new_utc), 108 | standards, 109 | ..Default::default() 110 | }, 111 | )?; 112 | Ok(buffer) 113 | } 114 | 115 | /// Get the current date and time in UTC. 116 | fn now() -> Option { 117 | let now = chrono::Local::now().naive_utc(); 118 | Datetime::from_ymd_hms( 119 | now.year(), 120 | now.month().try_into().ok()?, 121 | now.day().try_into().ok()?, 122 | now.hour().try_into().ok()?, 123 | now.minute().try_into().ok()?, 124 | now.second().try_into().ok()?, 125 | ) 126 | } 127 | 128 | /// An image format to export in. 129 | enum ImageExportFormat { 130 | Png, 131 | Svg, 132 | } 133 | 134 | /// Export the frames to PNGs or SVGs. 135 | fn export_image( 136 | document: &PagedDocument, 137 | fmt: ImageExportFormat, 138 | ppi: Option, 139 | ) -> StrResult>> { 140 | let mut buffers = Vec::new(); 141 | for page in &document.pages { 142 | let buffer = match fmt { 143 | ImageExportFormat::Png => typst_render::render(page, ppi.unwrap_or(144.0) / 72.0) 144 | .encode_png() 145 | .map_err(|err| eco_format!("failed to write PNG file ({err})"))?, 146 | ImageExportFormat::Svg => { 147 | let svg = typst_svg::svg(page); 148 | svg.as_bytes().to_vec() 149 | } 150 | }; 151 | buffers.push(buffer); 152 | } 153 | Ok(buffers) 154 | } 155 | 156 | /// Format diagnostic messages.\ 157 | pub fn format_diagnostics( 158 | world: &SystemWorld, 159 | errors: &[SourceDiagnostic], 160 | warnings: &[SourceDiagnostic], 161 | ) -> Result { 162 | let mut w = termcolor::Buffer::no_color(); 163 | 164 | let config = term::Config { 165 | tab_width: 2, 166 | ..Default::default() 167 | }; 168 | 169 | for diagnostic in warnings.iter().chain(errors.iter()) { 170 | let diag = match diagnostic.severity { 171 | Severity::Error => Diagnostic::error(), 172 | Severity::Warning => Diagnostic::warning(), 173 | } 174 | .with_message(diagnostic.message.clone()) 175 | .with_notes( 176 | diagnostic 177 | .hints 178 | .iter() 179 | .map(|e| (eco_format!("hint: {e}")).into()) 180 | .collect(), 181 | ) 182 | .with_labels(label(world, diagnostic.span).into_iter().collect()); 183 | 184 | term::emit_to_write_style(&mut w, &config, world, &diag)?; 185 | 186 | // Stacktrace-like helper diagnostics. 187 | for point in &diagnostic.trace { 188 | let message = point.v.to_string(); 189 | let help = Diagnostic::help() 190 | .with_message(message) 191 | .with_labels(label(world, point.span).into_iter().collect()); 192 | 193 | term::emit_to_write_style(&mut w, &config, world, &help)?; 194 | } 195 | } 196 | 197 | let s = String::from_utf8(w.into_inner()).unwrap(); 198 | Ok(s) 199 | } 200 | 201 | /// Create a label for a span. 202 | fn label(world: &SystemWorld, span: Span) -> Option> { 203 | Some(Label::primary(span.id()?, world.range(span)?)) 204 | } 205 | 206 | impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { 207 | type FileId = FileId; 208 | type Name = String; 209 | type Source = Lines; 210 | 211 | fn name(&'a self, id: FileId) -> CodespanResult { 212 | let vpath = id.vpath(); 213 | Ok(if let Some(package) = id.package() { 214 | format!("{package}{}", vpath.as_rooted_path().display()) 215 | } else { 216 | // Try to express the path relative to the working directory. 217 | vpath 218 | .resolve(self.root()) 219 | .and_then(|abs| pathdiff::diff_paths(abs, self.workdir())) 220 | .as_deref() 221 | .unwrap_or_else(|| vpath.as_rootless_path()) 222 | .to_string_lossy() 223 | .into() 224 | }) 225 | } 226 | 227 | fn source(&'a self, id: FileId) -> CodespanResult { 228 | Ok(self.lookup(id)) 229 | } 230 | 231 | fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult { 232 | let source = self.lookup(id); 233 | source 234 | .byte_to_line(given) 235 | .ok_or_else(|| CodespanError::IndexTooLarge { 236 | given, 237 | max: source.len_bytes(), 238 | }) 239 | } 240 | 241 | fn line_range(&'a self, id: FileId, given: usize) -> CodespanResult> { 242 | let source = self.lookup(id); 243 | source 244 | .line_to_range(given) 245 | .ok_or_else(|| CodespanError::LineTooLarge { 246 | given, 247 | max: source.len_lines(), 248 | }) 249 | } 250 | 251 | fn column_number(&'a self, id: FileId, _: usize, given: usize) -> CodespanResult { 252 | let source = self.lookup(id); 253 | source.byte_to_column(given).ok_or_else(|| { 254 | let max = source.len_bytes(); 255 | if given <= max { 256 | CodespanError::InvalidCharBoundary { given } 257 | } else { 258 | CodespanError::IndexTooLarge { given, max } 259 | } 260 | }) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /python/typst/__init__.pyi: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from typing import List, Optional, TypeVar, overload, Dict, Union, Literal, Tuple 3 | 4 | Input = TypeVar("Input", str, pathlib.Path, bytes) 5 | OutputFormat = Literal["pdf", "svg", "png", "html"] 6 | PathLike = TypeVar("PathLike", str, pathlib.Path) 7 | 8 | class TypstError(RuntimeError): 9 | """A structured error raised during Typst compilation or querying. 10 | 11 | This exception provides structured access to Typst diagnostics including 12 | error messages, hints, and stack traces. 13 | 14 | Attributes: 15 | message (str): The main error message 16 | hints (list[str]): List of helpful hints for resolving the error 17 | trace (list[str]): Stack trace information showing error location context 18 | """ 19 | 20 | message: str 21 | diagnostic: str 22 | hints: List[str] 23 | trace: List[str] 24 | 25 | def __init__( 26 | self, 27 | message: str, 28 | diagnostic: str, 29 | hints: Optional[List[str]] = None, 30 | trace: Optional[List[str]] = None, 31 | ) -> None: ... 32 | 33 | class TypstWarning(UserWarning): 34 | """A structured warning raised during Typst compilation. 35 | 36 | This warning provides structured access to Typst warning diagnostics including 37 | warning messages, hints, and stack traces. 38 | 39 | Attributes: 40 | message (str): The main warning message 41 | hints (list[str]): List of helpful hints related to the warning 42 | trace (list[str]): Stack trace information showing warning location context 43 | """ 44 | 45 | message: str 46 | diagnostic: str 47 | hints: List[str] 48 | trace: List[str] 49 | 50 | def __init__( 51 | self, 52 | message: str, 53 | diagnostic: str, 54 | hints: Optional[List[str]] = None, 55 | trace: Optional[List[str]] = None, 56 | ) -> None: ... 57 | 58 | class Fonts: 59 | def __init__( 60 | self, 61 | include_system_fonts: bool = True, 62 | include_embedded_fonts: bool = True, 63 | font_paths: List[Input] = [], 64 | ) -> None: ... 65 | 66 | class Compiler: 67 | def __init__( 68 | self, 69 | input: Optional[Input] = None, 70 | root: Optional[PathLike] = None, 71 | font_paths: Union[Fonts, List[Input]] = [], 72 | ignore_system_fonts: bool = False, 73 | sys_inputs: Dict[str, str] = {}, 74 | pdf_standards: Optional[ 75 | Union[Literal["1.7", "a-2b", "a-3b"], List[Literal["1.7", "a-2b", "a-3b"]]] 76 | ] = [], 77 | package_path: Optional[PathLike] = None, 78 | ) -> None: 79 | """Initialize a Typst compiler. 80 | Args: 81 | input: Optional .typ file bytes or path to project's main .typ file. Defaults to an empty in-memory document when omitted. 82 | root (Optional[PathLike], optional): Root path for the Typst project. 83 | font_paths (Union[Fonts, List[Input]]): Folders with fonts. 84 | ignore_system_fonts (bool): Ignore system fonts. 85 | sys_inputs (Dict[str, str]): string key-value pairs to be passed to the document via sys.inputs 86 | pdf_standards (Optional[Union[Literal["1.7", "a-2b", "a-3b"], List[Literal["1.7", "a-2b", "a-3b"]]]]): 87 | One or more PDF standard profiles to apply when exporting. Allowed values are `1.7`, `a-2b`, `a-3b`. 88 | package_path (Optional[PathLike]): Path to load local packages from. 89 | """ 90 | 91 | def compile( 92 | self, 93 | input: Optional[Input] = None, 94 | output: Optional[Input] = None, 95 | format: Optional[OutputFormat] = None, 96 | ppi: Optional[float] = None, 97 | ) -> Optional[Union[bytes, List[bytes]]]: 98 | """Compile a Typst project. 99 | Args: 100 | input: Optional .typ file bytes or path to compile for this invocation. 101 | output (Optional[PathLike], optional): Path to save the compiled file. 102 | Allowed extensions are `.pdf`, `.svg` and `.png` 103 | format (Optional[str]): Output format. 104 | Allowed values are `pdf`, `svg` and `png`. 105 | ppi (Optional[float]): Pixels per inch for PNG output, defaults to 144. 106 | Returns: 107 | Optional[Union[bytes, List[bytes]]]: Return the compiled file as `bytes` if output is `None`. 108 | """ 109 | 110 | def compile_with_warnings( 111 | self, 112 | input: Optional[Input] = None, 113 | output: Optional[Input] = None, 114 | format: Optional[OutputFormat] = None, 115 | ppi: Optional[float] = None, 116 | ) -> Tuple[Optional[Union[bytes, List[bytes]]], List[TypstWarning]]: 117 | """Compile a Typst project and return both result and warnings. 118 | Args: 119 | input: Optional .typ file bytes or path to compile for this invocation. 120 | output (Optional[PathLike], optional): Path to save the compiled file. 121 | Allowed extensions are `.pdf`, `.svg` and `.png` 122 | format (Optional[str]): Output format. 123 | Allowed values are `pdf`, `svg` and `png`. 124 | ppi (Optional[float]): Pixels per inch for PNG output, defaults to 144. 125 | Returns: 126 | Tuple[Optional[Union[bytes, List[bytes]]], List[TypstWarning]]: Return a tuple of (compiled_data, warnings). 127 | The first element is the compiled file as `bytes` if output is `None`, otherwise `None`. 128 | The second element is a list of structured warnings that occurred during compilation. 129 | """ 130 | 131 | def query( 132 | self, 133 | selector: str, 134 | field: Optional[str] = None, 135 | one: bool = False, 136 | format: Optional[Literal["json", "yaml"]] = None, 137 | ) -> str: 138 | """Query a Typst document. 139 | Args: 140 | selector (str): Typst selector like `