diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9daace18f..ea0c50541 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@1.65.0 + - uses: dtolnay/rust-toolchain@1.67.0 - uses: Swatinem/rust-cache@v2 - run: cargo check --workspace diff --git a/Cargo.toml b/Cargo.toml index 42a8973c9..3950687b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["cli"] [workspace.package] version = "0.4.0" -rust-version = "1.65" +rust-version = "1.67" authors = ["The Typst Project Developers"] edition = "2021" homepage = "https://typst.app" diff --git a/cli/src/args.rs b/cli/src/args.rs index 7eb4f4e2d..fbd5ec2da 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -62,7 +62,7 @@ pub struct CompileCommand { /// Path to input Typst file pub input: PathBuf, - /// Path to output PDF file + /// Path to output PDF file or PNG file(s) pub output: Option, /// Opens the output file after compilation using the default PDF viewer @@ -72,6 +72,10 @@ pub struct CompileCommand { /// Produces a flamegraph of the compilation process #[arg(long = "flamegraph", value_name = "OUTPUT_SVG")] pub flamegraph: Option>, + + /// The PPI to use if exported as PNG + #[arg(long = "ppi")] + pub ppi: Option, } /// List all discovered fonts in system and custom font paths diff --git a/cli/src/main.rs b/cli/src/main.rs index f2fcec0c5..8a3a753b0 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -25,8 +25,10 @@ use termcolor::{ColorChoice, StandardStream, WriteColor}; use time::macros::format_description; use time::Duration; use typst::diag::{FileError, FileResult, SourceError, StrResult}; +use typst::doc::Document; use typst::eval::{Datetime, Library}; use typst::font::{Font, FontBook, FontInfo, FontVariant}; +use typst::geom::Color; use typst::syntax::{Source, SourceId}; use typst::util::{Buffer, PathExt}; use typst::World; @@ -108,6 +110,9 @@ struct CompileSettings { /// The open command to use. open: Option>, + + /// The ppi to use for png export + ppi: Option, } impl CompileSettings { @@ -119,12 +124,13 @@ impl CompileSettings { root: Option, font_paths: Vec, open: Option>, + ppi: Option, ) -> Self { let output = match output { Some(path) => path, None => input.with_extension("pdf"), }; - Self { input, output, watch, root, font_paths, open } + Self { input, output, watch, root, font_paths, open, ppi } } /// Create a new compile settings from the CLI arguments and a compile command. @@ -133,12 +139,12 @@ impl CompileSettings { /// Panics if the command is not a compile or watch command. fn with_arguments(args: CliArguments) -> Self { let watch = matches!(args.command, Command::Watch(_)); - let CompileCommand { input, output, open, .. } = match args.command { + let CompileCommand { input, output, open, ppi, .. } = match args.command { Command::Compile(command) => command, Command::Watch(command) => command, _ => unreachable!(), }; - Self::new(input, output, watch, args.root, args.font_paths, open) + Self::new(input, output, watch, args.root, args.font_paths, open, ppi) } } @@ -261,12 +267,10 @@ fn compile_once(world: &mut SystemWorld, command: &CompileSettings) -> StrResult world.main = world.resolve(&command.input).map_err(|err| err.to_string())?; match typst::compile(world) { - // Export the PDF. + // Export the PDF / PNG. Ok(document) => { - let buffer = typst::export::pdf(&document); - fs::write(&command.output, buffer).map_err(|_| "failed to write PDF file")?; + export(&document, command)?; status(command, Status::Success).unwrap(); - tracing::info!("Compilation succeeded"); Ok(true) } @@ -277,13 +281,49 @@ fn compile_once(world: &mut SystemWorld, command: &CompileSettings) -> StrResult status(command, Status::Error).unwrap(); print_diagnostics(world, *errors) .map_err(|_| "failed to print diagnostics")?; - tracing::info!("Compilation failed"); Ok(false) } } } +/// Export into the target format. +fn export(document: &Document, command: &CompileSettings) -> StrResult<()> { + match command.output.extension() { + Some(ext) if ext.eq_ignore_ascii_case("png") => { + // Determine whether we have a `{n}` numbering. + let string = command.output.to_str().unwrap_or_default(); + let numbered = string.contains("{n}"); + if !numbered && document.pages.len() > 1 { + Err("cannot export multiple PNGs without `{n}` in output path")?; + } + + // Find a number width that accomodates all pages. For instance, the + // first page should be numbered "001" if there are between 100 and + // 999 pages. + let width = 1 + document.pages.len().checked_ilog10().unwrap_or(0) as usize; + let ppi = command.ppi.unwrap_or(2.0); + let mut storage; + + for (i, frame) in document.pages.iter().enumerate() { + let pixmap = typst::export::render(frame, ppi, Color::WHITE); + let path = if numbered { + storage = string.replace("{n}", &format!("{:0width$}", i + 1)); + Path::new(&storage) + } else { + command.output.as_path() + }; + pixmap.save_png(path).map_err(|_| "failed to write PNG file")?; + } + } + _ => { + let buffer = typst::export::pdf(document); + fs::write(&command.output, buffer).map_err(|_| "failed to write PDF file")?; + } + } + Ok(()) +} + /// Clear the terminal and render the status message. #[tracing::instrument(skip_all)] fn status(command: &CompileSettings, status: Status) -> io::Result<()> {