Skip to content

Commit

Permalink
Merge pull request #17 from OrangeX4/png-svg-exporter
Browse files Browse the repository at this point in the history
Add PNG and SVG exporter
  • Loading branch information
messense authored Jan 8, 2024
2 parents d1c177c + d1761e6 commit 12b5e27
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 8 deletions.
86 changes: 86 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ siphasher = "1.0"
tar = "0.4"
typst = { git = "https://github.com/typst/typst.git", tag = "v0.10.0" }
typst-pdf = { git = "https://github.com/typst/typst.git", tag = "v0.10.0" }
typst-svg = { git = "https://github.com/typst/typst.git", tag = "v0.10.0" }
typst-render = { git = "https://github.com/typst/typst.git", tag = "v0.10.0" }
ureq = { version = "2", default-features = false, features = [
"gzip",
"socks-proxy",
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ import typst
# Compile `hello.typ` to PDF and save as `hello.pdf`
typst.compile("hello.typ", output="hello.pdf")

# Compile `hello.typ` to PNG and save as `hello.png`
typst.compile("hello.typ", output="hello.png", format="png", ppi=144.0)

# Or return PDF content as bytes
pdf_bytes = typst.compile("hello.typ")

# Also for svg
svg_bytes = typst.compile("hello.typ", format="svg")
```

## License
Expand Down
10 changes: 9 additions & 1 deletion python/typst/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,25 @@ def compile(
output: PathLike,
root: Optional[PathLike] = None,
font_paths: List[PathLike] = [],
format: Optional[str] = None,
ppi: Optional[float] = None,
) -> None: ...

@overload
def compile(
input: PathLike,
output: None = None,
root: Optional[PathLike] = None,
font_paths: List[PathLike] = [],
format: Optional[str] = None,
ppi: Optional[float] = None,
) -> bytes: ...
def compile(
input: PathLike,
output: Optional[PathLike] = None,
root: Optional[PathLike] = None,
font_paths: List[PathLike] = [],
format: Optional[str] = None,
ppi: Optional[float] = None,
) -> Optional[bytes]:
"""Compile a Typst project.
Args:
Expand All @@ -31,6 +36,9 @@ def compile(
Allowed extensions are `.pdf`, `.svg` and `.png`
root (Optional[PathLike], optional): Root path for the Typst project.
font_paths (List[PathLike]): Folders with fonts.
format (Optional[str]): Output format.
Allowed values are `pdf`, `svg` and `png`.
ppi (Optional[float]): Pixels per inch for PNG output, defaults to 144.
Returns:
Optional[bytes]: Return the compiled file as `bytes` if output is `None`.
"""
41 changes: 39 additions & 2 deletions src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use typst::eval::Tracer;
use typst::foundations::Datetime;
use typst::model::Document;
use typst::syntax::{FileId, Source, Span};
use typst::visualize::Color;
use typst::{World, WorldExt};

use crate::world::SystemWorld;
Expand All @@ -15,7 +16,7 @@ type CodespanResult<T> = Result<T, CodespanError>;
type CodespanError = codespan_reporting::files::Error;

impl SystemWorld {
pub fn compile(&mut self) -> StrResult<Vec<u8>> {
pub fn compile(&mut self, format: Option<&str>, ppi: Option<f32>) -> StrResult<Vec<u8>> {
// Reset everything and ensure that the main file is present.
self.reset();
self.source(self.main()).map_err(|err| err.to_string())?;
Expand All @@ -26,7 +27,15 @@ impl SystemWorld {

match result {
// Export the PDF / PNG.
Ok(document) => Ok(export_pdf(&document, self)?),
Ok(document) => {
// Assert format is "pdf" or "png" or "svg"
match format.unwrap_or("pdf").to_ascii_lowercase().as_str() {
"pdf" => Ok(export_pdf(&document, self)?),
"png" => Ok(export_image(&document, ImageExportFormat::Png, ppi)?),
"svg" => Ok(export_image(&document, ImageExportFormat::Svg, ppi)?),
fmt => Err(eco_format!("unknown format: {fmt}")),
}
}
Err(errors) => Err(format_diagnostics(self, &errors, &warnings).unwrap().into()),
}
}
Expand All @@ -53,6 +62,34 @@ fn now() -> Option<Datetime> {
)
}

/// An image format to export in.
enum ImageExportFormat {
Png,
Svg,
}

/// Export the first frame to PNG or SVG.
fn export_image(
document: &Document,
fmt: ImageExportFormat,
ppi: Option<f32>,
) -> StrResult<Vec<u8>> {
// Find the first frame
let frame = document.pages.first().unwrap();
match fmt {
ImageExportFormat::Png => {
let pixmap = typst_render::render(frame, ppi.unwrap_or(144.0) / 72.0, Color::WHITE);
pixmap
.encode_png()
.map_err(|err| eco_format!("failed to write PNG file ({err})"))
}
ImageExportFormat::Svg => {
let svg = typst_svg::svg(frame);
Ok(svg.as_bytes().to_vec())
}
}
}

/// Format diagnostic messages.\
pub fn format_diagnostics(
world: &SystemWorld,
Expand Down
12 changes: 7 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ fn resources_path(py: Python<'_>, package: &str) -> PyResult<PathBuf> {

/// Compile a typst document to PDF
#[pyfunction]
#[pyo3(signature = (input, output = None, root = None, font_paths = Vec::new()))]
#[pyo3(signature = (input, output = None, root = None, font_paths = Vec::new(), format = None, ppi = None))]
fn compile(
py: Python<'_>,
input: PathBuf,
output: Option<PathBuf>,
root: Option<PathBuf>,
font_paths: Vec<PathBuf>,
format: Option<&str>,
ppi: Option<f32>,
) -> PyResult<PyObject> {
let input = input.canonicalize()?;
let root = if let Some(root) = root {
Expand Down Expand Up @@ -78,14 +80,14 @@ fn compile(
.font_files(default_fonts)
.build()
.map_err(|msg| PyRuntimeError::new_err(msg.to_string()))?;
let pdf_bytes = world
.compile()
let bytes = world
.compile(format, ppi)
.map_err(|msg| PyRuntimeError::new_err(msg.to_string()))?;
if let Some(output) = output {
std::fs::write(output, pdf_bytes)?;
std::fs::write(output, bytes)?;
Ok(Python::with_gil(|py| py.None()))
} else {
Ok(Python::with_gil(|py| PyBytes::new(py, &pdf_bytes).into()))
Ok(Python::with_gil(|py| PyBytes::new(py, &bytes).into()))
}
})
}
Expand Down

0 comments on commit 12b5e27

Please sign in to comment.