summaryrefslogtreecommitdiff
path: root/crates/typst-cli/src/compile.rs
diff options
context:
space:
mode:
authorIlia <43654815+istudyatuni@users.noreply.github.com>2024-04-01 22:36:21 +0300
committerGitHub <noreply@github.com>2024-04-01 19:36:21 +0000
commite91baaca820cc83bdd90d1e440c1c9fcd077942c (patch)
tree225015bb28e86f26d8c2e669e58318376eeb0b44 /crates/typst-cli/src/compile.rs
parent82717b28694d56a25f1bcb381258073255ef74cd (diff)
Support writing document to stdout (#3632)
Diffstat (limited to 'crates/typst-cli/src/compile.rs')
-rw-r--r--crates/typst-cli/src/compile.rs132
1 files changed, 87 insertions, 45 deletions
diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs
index 9a252a53..272ca292 100644
--- a/crates/typst-cli/src/compile.rs
+++ b/crates/typst-cli/src/compile.rs
@@ -1,5 +1,6 @@
use std::fs;
-use std::path::{Path, PathBuf};
+use std::io::Write;
+use std::path::Path;
use chrono::{Datelike, Timelike};
use codespan_reporting::diagnostic::{Diagnostic, Label};
@@ -16,7 +17,7 @@ use typst::syntax::{FileId, Source, Span};
use typst::visualize::Color;
use typst::{World, WorldExt};
-use crate::args::{CompileCommand, DiagnosticFormat, Input, OutputFormat};
+use crate::args::{CompileCommand, DiagnosticFormat, Input, Output, OutputFormat};
use crate::timings::Timer;
use crate::watch::Status;
use crate::world::SystemWorld;
@@ -27,18 +28,18 @@ type CodespanError = codespan_reporting::files::Error;
impl CompileCommand {
/// The output path.
- pub fn output(&self) -> PathBuf {
+ pub fn output(&self) -> Output {
self.output.clone().unwrap_or_else(|| {
let Input::Path(path) = &self.common.input else {
panic!("output must be specified when input is from stdin, as guarded by the CLI");
};
- path.with_extension(
+ Output::Path(path.with_extension(
match self.output_format().unwrap_or(OutputFormat::Pdf) {
OutputFormat::Pdf => "pdf",
OutputFormat::Png => "png",
OutputFormat::Svg => "svg",
},
- )
+ ))
})
}
@@ -48,7 +49,7 @@ impl CompileCommand {
pub fn output_format(&self) -> StrResult<OutputFormat> {
Ok(if let Some(specified) = self.format {
specified
- } else if let Some(output) = &self.output {
+ } else if let Some(Output::Path(output)) = &self.output {
match output.extension() {
Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
@@ -118,7 +119,9 @@ pub fn compile_once(
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
if let Some(open) = command.open.take() {
- open_file(open.as_deref(), &command.output())?;
+ if let Output::Path(file) = command.output() {
+ open_file(open.as_deref(), &file)?;
+ }
}
}
@@ -164,8 +167,9 @@ fn export(
/// Export to a PDF.
fn export_pdf(document: &Document, command: &CompileCommand) -> StrResult<()> {
let buffer = typst_pdf::pdf(document, Smart::Auto, now());
- let output = command.output();
- fs::write(output, buffer)
+ command
+ .output()
+ .write(&buffer)
.map_err(|err| eco_format!("failed to write PDF file ({err})"))?;
Ok(())
}
@@ -184,12 +188,13 @@ fn now() -> Option<Datetime> {
}
/// An image format to export in.
+#[derive(Clone, Copy)]
enum ImageExportFormat {
Png,
Svg,
}
-/// Export to one or multiple PNGs.
+/// Export to one or multiple images.
fn export_image(
world: &mut SystemWorld,
document: &Document,
@@ -199,10 +204,16 @@ fn export_image(
) -> StrResult<()> {
// Determine whether we have a `{n}` numbering.
let output = command.output();
- let string = output.to_str().unwrap_or_default();
- let numbered = string.contains("{n}");
- if !numbered && document.pages.len() > 1 {
- bail!("cannot export multiple images without `{{n}}` in output path");
+ let can_handle_multiple = match output {
+ Output::Stdout => false,
+ Output::Path(ref output) => output.to_str().unwrap_or_default().contains("{n}"),
+ };
+ if !can_handle_multiple && document.pages.len() > 1 {
+ let s = match output {
+ Output::Stdout => "to stdout",
+ Output::Path(_) => "without `{n}` in output path",
+ };
+ bail!("cannot export multiple images {s}");
}
// Find a number width that accommodates all pages. For instance, the
@@ -218,39 +229,33 @@ fn export_image(
.par_iter()
.enumerate()
.map(|(i, page)| {
- let storage;
- let path = if numbered {
- storage = string.replace("{n}", &format!("{:0width$}", i + 1));
- Path::new(&storage)
- } else {
- output.as_path()
- };
-
- // If we are not watching, don't use the cache.
- // If the frame is in the cache, skip it.
- // If the file does not exist, always create it.
- if watching && cache.is_cached(i, &page.frame) && path.exists() {
- return Ok(());
- }
-
- match fmt {
- ImageExportFormat::Png => {
- let pixmap = typst_render::render(
- &page.frame,
- command.ppi / 72.0,
- Color::WHITE,
- );
- pixmap
- .save_png(path)
- .map_err(|err| eco_format!("failed to write PNG file ({err})"))?;
- }
- ImageExportFormat::Svg => {
- let svg = typst_svg::svg(&page.frame);
- fs::write(path, svg.as_bytes())
- .map_err(|err| eco_format!("failed to write SVG file ({err})"))?;
+ // Use output with converted path.
+ let output = match output {
+ Output::Path(ref path) => {
+ let storage;
+ let path = if can_handle_multiple {
+ storage = path
+ .to_str()
+ .unwrap_or_default()
+ .replace("{n}", &format!("{:0width$}", i + 1));
+ Path::new(&storage)
+ } else {
+ path
+ };
+
+ // If we are not watching, don't use the cache.
+ // If the frame is in the cache, skip it.
+ // If the file does not exist, always create it.
+ if watching && cache.is_cached(i, &page.frame) && path.exists() {
+ return Ok(());
+ }
+
+ Output::Path(path.to_owned())
}
- }
+ Output::Stdout => Output::Stdout,
+ };
+ export_image_page(command, &page.frame, &output, fmt)?;
Ok(())
})
.collect::<Result<Vec<()>, EcoString>>()?;
@@ -258,6 +263,43 @@ fn export_image(
Ok(())
}
+/// Export single image.
+fn export_image_page(
+ command: &CompileCommand,
+ frame: &Frame,
+ output: &Output,
+ fmt: ImageExportFormat,
+) -> StrResult<()> {
+ match fmt {
+ ImageExportFormat::Png => {
+ let pixmap = typst_render::render(frame, command.ppi / 72.0, Color::WHITE);
+ let buf = pixmap
+ .encode_png()
+ .map_err(|err| eco_format!("failed to encode PNG file ({err})"))?;
+ output
+ .write(&buf)
+ .map_err(|err| eco_format!("failed to write PNG file ({err})"))?;
+ }
+ ImageExportFormat::Svg => {
+ let svg = typst_svg::svg(frame);
+ output
+ .write(svg.as_bytes())
+ .map_err(|err| eco_format!("failed to write SVG file ({err})"))?;
+ }
+ }
+ Ok(())
+}
+
+impl Output {
+ fn write(&self, buffer: &[u8]) -> StrResult<()> {
+ match self {
+ Output::Stdout => std::io::stdout().write_all(buffer),
+ Output::Path(path) => fs::write(path, buffer),
+ }
+ .map_err(|err| eco_format!("{err}"))
+ }
+}
+
/// Caches exported files so that we can avoid re-exporting them if they haven't
/// changed.
///