feat: support vscode tasks for exporting pdf, svg, and png (#488)

* feat: support vscode tasks for exporting pdf, svg, and png

* fix: parse errors

* dev: update fill, gap arguments

* fix: merged props
This commit is contained in:
Myriad-Dreamin 2024-08-04 23:41:52 +08:00 committed by GitHub
parent 60f3200088
commit 140299f0ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 609 additions and 50 deletions

View file

@ -135,12 +135,14 @@ mod polymorphic {
use super::prelude::*;
use super::*;
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PageSelection {
#[default]
First,
Merged,
Merged {
gap: Option<String>,
},
}
#[derive(Debug, Clone)]
@ -152,6 +154,8 @@ mod polymorphic {
page: PageSelection,
},
Png {
ppi: Option<f64>,
fill: Option<String>,
page: PageSelection,
},
}

View file

@ -20,20 +20,28 @@ use crate::tool::package::InitTask;
#[derive(Debug, Clone, Default, Deserialize)]
struct ExportOpts {
creation_timestamp: Option<String>,
fill: Option<String>,
ppi: Option<f64>,
page: PageSelection,
}
/// Here are implemented the handlers for each command.
impl LanguageState {
/// Export the current document as PDF file(s).
pub fn export_pdf(&mut self, req_id: RequestId, args: Vec<JsonValue>) -> ScheduledResult {
self.export(
req_id,
ExportKind::Pdf {
creation_timestamp: self.config.compile.determine_creation_timestamp(),
},
args,
)
pub fn export_pdf(&mut self, req_id: RequestId, mut args: Vec<JsonValue>) -> ScheduledResult {
let opts = get_arg_or_default!(args[1] as ExportOpts);
let creation_timestamp = if let Some(value) = opts.creation_timestamp {
Some(
parse_source_date_epoch(&value)
.map_err(|e| invalid_params(format!("Cannot parse creation timestamp: {e}")))?,
)
} else {
self.config.compile.determine_creation_timestamp()
};
self.export(req_id, ExportKind::Pdf { creation_timestamp }, args)
}
/// Export the current document as Svg file(s).
@ -45,7 +53,15 @@ impl LanguageState {
/// Export the current document as Png file(s).
pub fn export_png(&mut self, req_id: RequestId, mut args: Vec<JsonValue>) -> ScheduledResult {
let opts = get_arg_or_default!(args[1] as ExportOpts);
self.export(req_id, ExportKind::Png { page: opts.page }, args)
self.export(
req_id,
ExportKind::Png {
fill: opts.fill,
ppi: opts.ppi,
page: opts.page,
},
args,
)
}
/// Export the current document as some format. The client is responsible

View file

@ -1,12 +1,18 @@
//! The actor that handles various document export, like PDF and SVG export.
use std::str::FromStr;
use std::{path::PathBuf, sync::Arc};
use anyhow::{bail, Context};
use once_cell::sync::Lazy;
use tinymist_query::{ExportKind, PageSelection};
use tokio::sync::mpsc;
use typst::{foundations::Smart, layout::Abs, layout::Frame, visualize::Color};
use typst::{
foundations::Smart,
layout::{Abs, Frame},
syntax::{ast, SyntaxNode},
visualize::Color,
};
use typst_ts_compiler::{EntryReader, EntryState, TaskInputs};
use typst_ts_core::TypstDatetime;
@ -205,12 +211,52 @@ impl ExportConfig {
typst_pdf::pdf(doc, Smart::Auto, timestamp)
}
Svg { page: First } => typst_svg::svg(first_frame()).into_bytes(),
Svg { page: Merged } => typst_svg::svg_merged(doc, Abs::zero()).into_bytes(),
Png { page: First } => typst_render::render(first_frame(), 3., Color::WHITE)
.encode_png()
.map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))?,
Png { page: Merged } => {
typst_render::render_merged(doc, 3., Color::WHITE, Abs::zero(), Color::WHITE)
Svg {
page: Merged { .. },
} => typst_svg::svg_merged(doc, Abs::zero()).into_bytes(),
Png {
ppi,
fill,
page: First,
} => {
let ppi = ppi.unwrap_or(144.) as f32;
if ppi <= 1e-6 {
bail!("invalid ppi: {ppi}");
}
let fill = if let Some(fill) = fill {
parse_color(fill).map_err(|err| anyhow::anyhow!("invalid fill ({err})"))?
} else {
Color::WHITE
};
typst_render::render(first_frame(), ppi / 72., fill)
.encode_png()
.map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))?
}
Png {
ppi,
fill,
page: Merged { gap },
} => {
let ppi = ppi.unwrap_or(144.) as f32;
if ppi <= 1e-6 {
bail!("invalid ppi: {ppi}");
}
let fill = if let Some(fill) = fill {
parse_color(fill).map_err(|err| anyhow::anyhow!("invalid fill ({err})"))?
} else {
Color::WHITE
};
let gap = if let Some(gap) = gap {
parse_length(gap).map_err(|err| anyhow::anyhow!("invalid gap ({err})"))?
} else {
Abs::zero()
};
typst_render::render_merged(doc, ppi / 72., fill, gap, fill)
.encode_png()
.map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))?
}
@ -226,6 +272,53 @@ impl ExportConfig {
}
}
fn parse_color(fill: String) -> anyhow::Result<Color> {
match fill.as_str() {
"black" => Ok(Color::BLACK),
"white" => Ok(Color::WHITE),
"red" => Ok(Color::RED),
"green" => Ok(Color::GREEN),
"blue" => Ok(Color::BLUE),
hex if hex.starts_with('#') => {
Color::from_str(&hex[1..]).map_err(|e| anyhow::anyhow!("failed to parse color: {e}"))
}
_ => bail!("invalid color: {fill}"),
}
}
fn parse_length(gap: String) -> anyhow::Result<Abs> {
let length = typst::syntax::parse_code(&gap);
if length.erroneous() {
bail!("invalid length: {gap}, errors: {:?}", length.errors());
}
let length: Option<ast::Numeric> = descendants(&length).into_iter().find_map(SyntaxNode::cast);
let Some(length) = length else {
bail!("not a length: {gap}");
};
let (value, unit) = length.get();
match unit {
ast::Unit::Pt => Ok(Abs::pt(value)),
ast::Unit::Mm => Ok(Abs::mm(value)),
ast::Unit::Cm => Ok(Abs::cm(value)),
ast::Unit::In => Ok(Abs::inches(value)),
_ => bail!("invalid unit: {unit:?} in {gap}"),
}
}
/// Low performance but simple recursive iterator.
fn descendants(node: &SyntaxNode) -> impl IntoIterator<Item = &SyntaxNode> + '_ {
let mut res = vec![];
for child in node.children() {
res.push(child);
res.extend(descendants(child));
}
res
}
fn log_err<T>(artifact: anyhow::Result<T>) -> Option<T> {
match artifact {
Ok(v) => Some(v),
@ -259,4 +352,36 @@ mod tests {
assert!(!conf.count_words);
assert_eq!(conf.config.mode, ExportMode::Never);
}
#[test]
fn test_parse_length() {
assert_eq!(parse_length("1pt".to_owned()).unwrap(), Abs::pt(1.));
assert_eq!(parse_length("1mm".to_owned()).unwrap(), Abs::mm(1.));
assert_eq!(parse_length("1cm".to_owned()).unwrap(), Abs::cm(1.));
assert_eq!(parse_length("1in".to_owned()).unwrap(), Abs::inches(1.));
assert!(parse_length("1".to_owned()).is_err());
assert!(parse_length("1px".to_owned()).is_err());
}
#[test]
fn test_parse_color() {
assert_eq!(parse_color("black".to_owned()).unwrap(), Color::BLACK);
assert_eq!(parse_color("white".to_owned()).unwrap(), Color::WHITE);
assert_eq!(parse_color("red".to_owned()).unwrap(), Color::RED);
assert_eq!(parse_color("green".to_owned()).unwrap(), Color::GREEN);
assert_eq!(parse_color("blue".to_owned()).unwrap(), Color::BLUE);
assert_eq!(
parse_color("#000000".to_owned()).unwrap().to_hex(),
"#000000"
);
assert_eq!(
parse_color("#ffffff".to_owned()).unwrap().to_hex(),
"#ffffff"
);
assert_eq!(
parse_color("#000000cc".to_owned()).unwrap().to_hex(),
"#000000cc"
);
assert!(parse_color("invalid".to_owned()).is_err());
}
}

View file

@ -16,13 +16,19 @@ impl<T> fmt::Debug for Derived<T> {
macro_rules! get_arg {
($args:ident[$idx:expr] as $ty:ty) => {{
let arg = $args.get_mut($idx);
let arg = arg.and_then(|x| from_value::<$ty>(x.take()).ok());
match arg {
let arg = match arg {
Some(v) => v,
None => {
let msg = concat!("expect ", stringify!($ty), "at args[", $idx, "]");
let msg = concat!("expect ", stringify!($ty), " at args[", $idx, "]");
return Err(invalid_params(msg));
}
};
match from_value::<$ty>(arg.take()) {
Ok(v) => v,
Err(err) => {
let msg = concat!("expect ", stringify!($ty), " at args[", $idx, "], error: ");
return Err(invalid_params(format!("{}{}", msg, err)));
}
}
}};
}

View file

@ -136,7 +136,7 @@ fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
}
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, String> {
pub fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, String> {
let timestamp: i64 = raw
.parse()
.map_err(|err| format!("timestamp must be decimal integer ({err})"))?;