From 7fe13415168233019406e3e5276a79d4cce0d60f Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Wed, 3 Jan 2024 23:40:09 +0800 Subject: [PATCH 1/3] feat: add png and svg exporter --- Cargo.lock | 86 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ src/compiler.rs | 41 +++++++++++++++++++++-- src/lib.rs | 12 ++++--- 4 files changed, 134 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7f303c..d7c3e38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1260,6 +1260,15 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pixglyph" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67591f21f6668e63c1cd85adab066ac8a92bc7b962668dd8042197a6e4b8f8f" +dependencies = [ + "ttf-parser 0.19.2", +] + [[package]] name = "pkg-config" version = "0.3.27" @@ -1538,6 +1547,32 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "resvg" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7980f653f9a7db31acff916a262c3b78c562919263edea29bf41a056e20497" +dependencies = [ + "gif", + "jpeg-decoder", + "log", + "pico-args", + "png", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.6" @@ -2010,6 +2045,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a067b809476893fce6a254cf285850ff69c847e6cfbade6a20b655b6c7e80d" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + [[package]] name = "tiny-skia-path" version = "0.11.3" @@ -2238,10 +2288,46 @@ dependencies = [ "tar", "typst", "typst-pdf", + "typst-render", + "typst-svg", "ureq", "walkdir", ] +[[package]] +name = "typst-render" +version = "0.10.0" +source = "git+https://github.com/typst/typst.git?tag=v0.10.0#70ca0d257bb4ba927f63260e20443f244e0bb58c" +dependencies = [ + "bytemuck", + "comemo", + "flate2", + "image", + "pixglyph", + "resvg", + "roxmltree", + "tiny-skia", + "ttf-parser 0.19.2", + "typst", + "usvg", +] + +[[package]] +name = "typst-svg" +version = "0.10.0" +source = "git+https://github.com/typst/typst.git?tag=v0.10.0#70ca0d257bb4ba927f63260e20443f244e0bb58c" +dependencies = [ + "base64", + "comemo", + "ecow", + "flate2", + "tracing", + "ttf-parser 0.19.2", + "typst", + "xmlparser", + "xmlwriter", +] + [[package]] name = "typst-syntax" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index 12ef454..a2aadac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/src/compiler.rs b/src/compiler.rs index 2f273bd..6603a71 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -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; @@ -15,7 +16,7 @@ type CodespanResult = Result; type CodespanError = codespan_reporting::files::Error; impl SystemWorld { - pub fn compile(&mut self) -> StrResult> { + pub fn compile(&mut self, format: Option<&str>, ppi: Option) -> StrResult> { // Reset everything and ensure that the main file is present. self.reset(); self.source(self.main()).map_err(|err| err.to_string())?; @@ -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()), } } @@ -53,6 +62,34 @@ fn now() -> Option { ) } +/// 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, +) -> StrResult> { + // 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, diff --git a/src/lib.rs b/src/lib.rs index b258b85..4d9fc4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,13 +41,15 @@ fn resources_path(py: Python<'_>, package: &str) -> PyResult { /// 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, root: Option, font_paths: Vec, + format: Option<&str>, + ppi: Option, ) -> PyResult { let input = input.canonicalize()?; let root = if let Some(root) = root { @@ -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())) } }) } From b45d46dee3fe3b26c392af9df9d4f5a8ef29b6f1 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Wed, 3 Jan 2024 23:42:10 +0800 Subject: [PATCH 2/3] docs: update docs for png and svg exporter --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 73cf3ee..ef090d6 100644 --- a/README.md +++ b/README.md @@ -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 From d1761e61bbf39b828e42c051b942bfbb10f5f00c Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Thu, 4 Jan 2024 15:00:24 +0800 Subject: [PATCH 3/3] feat: update python interface info --- python/typst/__init__.pyi | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/python/typst/__init__.pyi b/python/typst/__init__.pyi index ce82477..a80e47d 100644 --- a/python/typst/__init__.pyi +++ b/python/typst/__init__.pyi @@ -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: @@ -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`. """