mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Support writing document to stdout (#3632)
This commit is contained in:
parent
82717b2869
commit
e91baaca82
@ -67,15 +67,16 @@ pub struct CompileCommand {
|
||||
#[clap(flatten)]
|
||||
pub common: SharedArgs,
|
||||
|
||||
/// Path to output file (PDF, PNG, or SVG)
|
||||
#[clap(required_if_eq("input", "-"))]
|
||||
pub output: Option<PathBuf>,
|
||||
/// Path to output file (PDF, PNG, or SVG), use `-` to write output to stdout
|
||||
#[clap(required_if_eq("input", "-"), value_parser = ValueParser::new(output_value_parser))]
|
||||
pub output: Option<Output>,
|
||||
|
||||
/// The format of the output file, inferred from the extension by default
|
||||
#[arg(long = "format", short = 'f')]
|
||||
pub format: Option<OutputFormat>,
|
||||
|
||||
/// Opens the output file using the default viewer after compilation
|
||||
/// Opens the output file using the default viewer after compilation.
|
||||
/// Ignored if output is stdout
|
||||
#[arg(long = "open")]
|
||||
pub open: Option<Option<String>>,
|
||||
|
||||
@ -184,6 +185,24 @@ pub enum Input {
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
/// An output that is either stdout or a real path.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Output {
|
||||
/// Stdout, represented by `-`.
|
||||
Stdout,
|
||||
/// A non-empty path.
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
impl Display for Output {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Output::Stdout => f.pad("stdout"),
|
||||
Output::Path(path) => path.display().fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The clap value parser used by `SharedArgs.input`
|
||||
fn input_value_parser(value: &str) -> Result<Input, clap::error::Error> {
|
||||
if value.is_empty() {
|
||||
@ -195,6 +214,18 @@ fn input_value_parser(value: &str) -> Result<Input, clap::error::Error> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The clap value parser used by `CompileCommand.output`
|
||||
fn output_value_parser(value: &str) -> Result<Output, clap::error::Error> {
|
||||
// Empty value also handled by clap for `Option<Output>`
|
||||
if value.is_empty() {
|
||||
Err(clap::Error::new(clap::error::ErrorKind::InvalidValue))
|
||||
} else if value == "-" {
|
||||
Ok(Output::Stdout)
|
||||
} else {
|
||||
Ok(Output::Path(value.into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses key/value pairs split by the first equal sign.
|
||||
///
|
||||
/// This function will return an error if the argument contains no equals sign
|
||||
|
@ -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()
|
||||
// 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,
|
||||
};
|
||||
|
||||
// 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})"))?;
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
///
|
||||
|
@ -10,9 +10,9 @@ use codespan_reporting::term::{self, termcolor};
|
||||
use ecow::eco_format;
|
||||
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _};
|
||||
use same_file::is_same_file;
|
||||
use typst::diag::StrResult;
|
||||
use typst::diag::{bail, StrResult};
|
||||
|
||||
use crate::args::{CompileCommand, Input};
|
||||
use crate::args::{CompileCommand, Input, Output};
|
||||
use crate::compile::compile_once;
|
||||
use crate::timings::Timer;
|
||||
use crate::world::{SystemWorld, WorldCreationError};
|
||||
@ -20,8 +20,12 @@ use crate::{print_error, terminal};
|
||||
|
||||
/// Execute a watching compilation command.
|
||||
pub fn watch(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
|
||||
let Output::Path(output) = command.output() else {
|
||||
bail!("cannot write document to stdout in watch mode");
|
||||
};
|
||||
|
||||
// Create a file system watcher.
|
||||
let mut watcher = Watcher::new(command.output())?;
|
||||
let mut watcher = Watcher::new(output)?;
|
||||
|
||||
// Create the world that serves sources, files, and fonts.
|
||||
// Additionally, if any files do not exist, wait until they do.
|
||||
@ -281,7 +285,7 @@ impl Status {
|
||||
out.set_color(&color)?;
|
||||
write!(out, "writing to")?;
|
||||
out.reset()?;
|
||||
writeln!(out, " {}", output.display())?;
|
||||
writeln!(out, " {output}")?;
|
||||
|
||||
writeln!(out)?;
|
||||
writeln!(out, "[{timestamp}] {}", self.message())?;
|
||||
|
Loading…
x
Reference in New Issue
Block a user