feat(cli): export as png (#1159)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Lino Le Van 2023-05-23 03:20:12 -07:00 committed by GitHub
parent 5dbc15ef0c
commit 5400570efa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 55 additions and 11 deletions

View File

@ -32,6 +32,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@1.65.0 - uses: dtolnay/rust-toolchain@1.67.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo check --workspace - run: cargo check --workspace

View File

@ -4,7 +4,7 @@ default-members = ["cli"]
[workspace.package] [workspace.package]
version = "0.4.0" version = "0.4.0"
rust-version = "1.65" rust-version = "1.67"
authors = ["The Typst Project Developers"] authors = ["The Typst Project Developers"]
edition = "2021" edition = "2021"
homepage = "https://typst.app" homepage = "https://typst.app"

View File

@ -62,7 +62,7 @@ pub struct CompileCommand {
/// Path to input Typst file /// Path to input Typst file
pub input: PathBuf, pub input: PathBuf,
/// Path to output PDF file /// Path to output PDF file or PNG file(s)
pub output: Option<PathBuf>, pub output: Option<PathBuf>,
/// Opens the output file after compilation using the default PDF viewer /// 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 /// Produces a flamegraph of the compilation process
#[arg(long = "flamegraph", value_name = "OUTPUT_SVG")] #[arg(long = "flamegraph", value_name = "OUTPUT_SVG")]
pub flamegraph: Option<Option<PathBuf>>, pub flamegraph: Option<Option<PathBuf>>,
/// The PPI to use if exported as PNG
#[arg(long = "ppi")]
pub ppi: Option<f32>,
} }
/// List all discovered fonts in system and custom font paths /// List all discovered fonts in system and custom font paths

View File

@ -25,8 +25,10 @@ use termcolor::{ColorChoice, StandardStream, WriteColor};
use time::macros::format_description; use time::macros::format_description;
use time::Duration; use time::Duration;
use typst::diag::{FileError, FileResult, SourceError, StrResult}; use typst::diag::{FileError, FileResult, SourceError, StrResult};
use typst::doc::Document;
use typst::eval::{Datetime, Library}; use typst::eval::{Datetime, Library};
use typst::font::{Font, FontBook, FontInfo, FontVariant}; use typst::font::{Font, FontBook, FontInfo, FontVariant};
use typst::geom::Color;
use typst::syntax::{Source, SourceId}; use typst::syntax::{Source, SourceId};
use typst::util::{Buffer, PathExt}; use typst::util::{Buffer, PathExt};
use typst::World; use typst::World;
@ -108,6 +110,9 @@ struct CompileSettings {
/// The open command to use. /// The open command to use.
open: Option<Option<String>>, open: Option<Option<String>>,
/// The ppi to use for png export
ppi: Option<f32>,
} }
impl CompileSettings { impl CompileSettings {
@ -119,12 +124,13 @@ impl CompileSettings {
root: Option<PathBuf>, root: Option<PathBuf>,
font_paths: Vec<PathBuf>, font_paths: Vec<PathBuf>,
open: Option<Option<String>>, open: Option<Option<String>>,
ppi: Option<f32>,
) -> Self { ) -> Self {
let output = match output { let output = match output {
Some(path) => path, Some(path) => path,
None => input.with_extension("pdf"), 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. /// 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. /// Panics if the command is not a compile or watch command.
fn with_arguments(args: CliArguments) -> Self { fn with_arguments(args: CliArguments) -> Self {
let watch = matches!(args.command, Command::Watch(_)); 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::Compile(command) => command,
Command::Watch(command) => command, Command::Watch(command) => command,
_ => unreachable!(), _ => 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())?; world.main = world.resolve(&command.input).map_err(|err| err.to_string())?;
match typst::compile(world) { match typst::compile(world) {
// Export the PDF. // Export the PDF / PNG.
Ok(document) => { Ok(document) => {
let buffer = typst::export::pdf(&document); export(&document, command)?;
fs::write(&command.output, buffer).map_err(|_| "failed to write PDF file")?;
status(command, Status::Success).unwrap(); status(command, Status::Success).unwrap();
tracing::info!("Compilation succeeded"); tracing::info!("Compilation succeeded");
Ok(true) Ok(true)
} }
@ -277,13 +281,49 @@ fn compile_once(world: &mut SystemWorld, command: &CompileSettings) -> StrResult
status(command, Status::Error).unwrap(); status(command, Status::Error).unwrap();
print_diagnostics(world, *errors) print_diagnostics(world, *errors)
.map_err(|_| "failed to print diagnostics")?; .map_err(|_| "failed to print diagnostics")?;
tracing::info!("Compilation failed"); tracing::info!("Compilation failed");
Ok(false) 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. /// Clear the terminal and render the status message.
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
fn status(command: &CompileSettings, status: Status) -> io::Result<()> { fn status(command: &CompileSettings, status: Status) -> io::Result<()> {