Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parameter to select pages to be exported by CLI #4039

Merged
merged 23 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
64 changes: 64 additions & 0 deletions crates/typst-cli/src/args.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use std::fmt::{self, Display, Formatter};
use std::num::NonZeroUsize;
use std::ops::RangeInclusive;
use std::path::PathBuf;
use std::str::FromStr;

use chrono::{DateTime, Utc};
use clap::builder::ValueParser;
Expand Down Expand Up @@ -76,6 +79,18 @@ pub struct CompileCommand {
#[clap(required_if_eq("input", "-"), value_parser = ValueParser::new(output_value_parser))]
pub output: Option<Output>,

/// Which pages to export. When unspecified, all document pages are exported.
///
/// Pages to export are separated by commas, and can be either simple page
/// numbers (e.g. '2,5' to export only pages 2 and 5) or page ranges
/// (e.g. '2,3-6,8-' to export page 2, pages 3 to 6 (inclusive), page 8 and
/// any pages after it).
///
/// Page numbers are one-indexed and correspond to real page numbers in the
/// document (therefore not being affected by the document's page counter).
#[arg(long = "pages", value_delimiter = ',')]
pub pages: Option<Vec<PageRangeArgument>>,

/// Output a Makefile rule describing the current compilation
#[clap(long = "make-deps", value_name = "PATH")]
pub make_deps: Option<PathBuf>,
Expand Down Expand Up @@ -271,6 +286,55 @@ fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
Ok((key, val))
}

/// Implements parsing of page ranges (`1-3`, `4`, `5-`, `-2`), used by the
/// `CompileCommand.pages` argument, through the `FromStr` trait instead of
/// a value parser, in order to generate better errors.
///
/// See also: https://github.com/clap-rs/clap/issues/5065
#[derive(Debug, Clone)]
pub struct PageRangeArgument(RangeInclusive<Option<NonZeroUsize>>);

impl PageRangeArgument {
pub fn to_range(&self) -> RangeInclusive<Option<NonZeroUsize>> {
self.0.clone()
}
}

impl FromStr for PageRangeArgument {
type Err = &'static str;

fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.split('-').map(str::trim).collect::<Vec<_>>().as_slice() {
[] | [""] => Err("page export range must not be empty"),
[single_page] => {
let page_number = parse_page_number(single_page)?;
Ok(PageRangeArgument(Some(page_number)..=Some(page_number)))
}
["", ""] => Err("page export range must have start or end"),
[start, ""] => Ok(PageRangeArgument(Some(parse_page_number(start)?)..=None)),
["", end] => Ok(PageRangeArgument(None..=Some(parse_page_number(end)?))),
[start, end] => {
let start = parse_page_number(start)?;
let end = parse_page_number(end)?;
if start > end {
Err("page export range must end at a page after the start")
} else {
Ok(PageRangeArgument(Some(start)..=Some(end)))
}
}
[_, _, _, ..] => Err("page export range must have a single hyphen"),
}
}
}

fn parse_page_number(value: &str) -> Result<NonZeroUsize, &'static str> {
if value == "0" {
Err("page numbers start at one")
} else {
NonZeroUsize::from_str(value).map_err(|_| "not a valid page number")
}
}

/// Lists all discovered fonts in system and custom font paths
#[derive(Debug, Clone, Parser)]
pub struct FontsCommand {
Expand Down
44 changes: 35 additions & 9 deletions crates/typst-cli/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term;
use ecow::{eco_format, EcoString};
use parking_lot::RwLock;
use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use typst::diag::{bail, At, Severity, SourceDiagnostic, StrResult};
use typst::eval::Tracer;
use typst::foundations::{Datetime, Smart};
use typst::layout::Frame;
use typst::layout::{Frame, PageRanges};
use typst::model::Document;
use typst::syntax::{FileId, Source, Span};
use typst::visualize::Color;
use typst::{World, WorldExt};

use crate::args::{CompileCommand, DiagnosticFormat, Input, Output, OutputFormat};
use crate::args::{
CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, PageRangeArgument,
};
use crate::timings::Timer;
use crate::watch::Status;
use crate::world::SystemWorld;
Expand Down Expand Up @@ -60,6 +62,17 @@ impl CompileCommand {
OutputFormat::Pdf
})
}

/// The ranges of the pages to be exported as specified by the user.
///
/// This returns `None` if all pages should be exported.
pub fn exported_page_ranges(&self) -> Option<PageRanges> {
self.pages.as_ref().map(|export_ranges| {
PageRanges::new(
export_ranges.iter().map(PageRangeArgument::to_range).collect(),
)
})
}
}

/// Execute a compilation command.
Expand Down Expand Up @@ -171,7 +184,8 @@ fn export_pdf(document: &Document, command: &CompileCommand) -> StrResult<()> {
let timestamp = convert_datetime(
command.common.creation_timestamp.unwrap_or_else(chrono::Utc::now),
);
let buffer = typst_pdf::pdf(document, Smart::Auto, timestamp);
let exported_page_ranges = command.exported_page_ranges();
let buffer = typst_pdf::pdf(document, Smart::Auto, timestamp, exported_page_ranges);
command
.output()
.write(&buffer)
Expand Down Expand Up @@ -214,7 +228,21 @@ fn export_image(
output_template::has_indexable_template(output.to_str().unwrap_or_default())
}
};
if !can_handle_multiple && document.pages.len() > 1 {

let exported_page_ranges = command.exported_page_ranges();

let exported_pages = document
.pages
.iter()
.enumerate()
.filter(|(i, _)| {
exported_page_ranges.as_ref().map_or(true, |exported_page_ranges| {
exported_page_ranges.includes_page_index(*i)
})
})
.collect::<Vec<_>>();

if !can_handle_multiple && exported_pages.len() > 1 {
let err = match output {
Output::Stdout => "to stdout",
Output::Path(_) => {
Expand All @@ -227,10 +255,8 @@ fn export_image(
let cache = world.export_cache();

// The results are collected in a `Vec<()>` which does not allocate.
document
.pages
exported_pages
.par_iter()
.enumerate()
.map(|(i, page)| {
// Use output with converted path.
let output = match output {
Expand All @@ -250,7 +276,7 @@ fn export_image(
// 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() {
if watching && cache.is_cached(*i, &page.frame) && path.exists() {
return Ok(());
}

Expand Down
25 changes: 16 additions & 9 deletions crates/typst-pdf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use pdf_writer::writers::Destination;
use pdf_writer::{Finish, Name, Pdf, Rect, Ref, Str, TextStr};
use typst::foundations::{Datetime, Label, NativeElement, Smart};
use typst::introspection::Location;
use typst::layout::{Abs, Dir, Em, Frame, Transform};
use typst::layout::{Abs, Dir, Em, Frame, PageRanges, Transform};
use typst::model::{Document, HeadingElem};
use typst::text::color::frame_for_glyph;
use typst::text::{Font, Lang};
Expand Down Expand Up @@ -55,13 +55,17 @@ use crate::pattern::PdfPattern;
/// The `timestamp`, if given, is expected to be the creation date of the
/// document as a UTC datetime. It will only be used if `set document(date: ..)`
/// is `auto`.
///
/// The `page_ranges` option specifies which ranges of pages should be exported
/// in the PDF. When `None`, all pages should be exported.
#[typst_macros::time(name = "pdf")]
pub fn pdf(
document: &Document,
ident: Smart<&str>,
timestamp: Option<Datetime>,
page_ranges: Option<PageRanges>,
) -> Vec<u8> {
let mut ctx = PdfContext::new(document);
let mut ctx = PdfContext::new(document, page_ranges);
page::construct_pages(&mut ctx, &document.pages);
font::write_fonts(&mut ctx);
image::write_images(&mut ctx);
Expand All @@ -82,7 +86,10 @@ struct PdfContext<'a> {
/// The writer we are writing the PDF into.
pdf: Pdf,
/// Content of exported pages.
pages: Vec<EncodedPage>,
pages: Vec<Option<EncodedPage>>,
/// Page ranges to export.
/// When `None`, all pages are exported.
exported_pages: Option<PageRanges>,
/// For each font a mapping from used glyphs to their text representation.
/// May contain multiple chars in case of ligatures or similar things. The
/// same glyph can have a different text representation within one document,
Expand All @@ -108,8 +115,6 @@ struct PdfContext<'a> {
/// dictionary), which Acrobat doesn't appreciate (it fails to parse the
/// font) even if the specification seems to allow it.
type3_font_resources_ref: Ref,
/// The IDs of written pages.
page_refs: Vec<Ref>,
/// The IDs of written fonts.
font_refs: Vec<Ref>,
/// The IDs of written images.
Expand Down Expand Up @@ -145,7 +150,7 @@ struct PdfContext<'a> {
}

impl<'a> PdfContext<'a> {
fn new(document: &'a Document) -> Self {
fn new(document: &'a Document, page_ranges: Option<PageRanges>) -> Self {
let mut alloc = Ref::new(1);
let page_tree_ref = alloc.bump();
let global_resources_ref = alloc.bump();
Expand All @@ -154,13 +159,13 @@ impl<'a> PdfContext<'a> {
document,
pdf: Pdf::new(),
pages: vec![],
exported_pages: page_ranges,
glyph_sets: HashMap::new(),
languages: BTreeMap::new(),
alloc,
page_tree_ref,
global_resources_ref,
type3_font_resources_ref,
page_refs: vec![],
font_refs: vec![],
image_refs: vec![],
gradient_refs: vec![],
Expand Down Expand Up @@ -251,7 +256,8 @@ fn write_catalog(ctx: &mut PdfContext, ident: Smart<&str>, timestamp: Option<Dat
}

info.finish();
xmp.num_pages(ctx.document.pages.len() as u32);
// Only count exported pages.
xmp.num_pages(ctx.pages.iter().filter(|page| page.is_some()).count() as u32);
xmp.format("application/pdf");
xmp.language(ctx.languages.keys().map(|lang| LangId(lang.as_str())));

Expand Down Expand Up @@ -350,7 +356,8 @@ fn write_named_destinations(ctx: &mut PdfContext) {
let index = pos.page.get() - 1;
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());

if let Some(page) = ctx.pages.get(index) {
// If the heading's page exists and is exported, include it.
if let Some(Some(page)) = ctx.pages.get(index) {
let dest_ref = ctx.alloc.bump();
let x = pos.point.x.to_f32();
let y = (page.size.y - y).to_f32();
Expand Down
16 changes: 14 additions & 2 deletions crates/typst-pdf/src/outline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,17 @@ pub(crate) fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
// enforced in the manner shown below.
let mut last_skipped_level = None;
let elements = ctx.document.introspector.query(&HeadingElem::elem().select());

for elem in elements.iter() {
if let Some(page_ranges) = &ctx.exported_pages {
if !page_ranges
.includes_page(ctx.document.introspector.page(elem.location().unwrap()))
{
// Don't bookmark headings in non-exported pages
continue;
}
}

let heading = elem.to_packed::<HeadingElem>().unwrap();
let leaf = HeadingNode::leaf(heading);

Expand Down Expand Up @@ -166,9 +176,11 @@ fn write_outline_item(
let loc = node.element.location().unwrap();
let pos = ctx.document.introspector.position(loc);
let index = pos.page.get() - 1;
if let Some(page) = ctx.pages.get(index) {

// Don't link to non-exported pages.
if let Some(Some(page)) = ctx.pages.get(index) {
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
outline.dest().page(ctx.page_refs[index]).xyz(
outline.dest().page(page.id).xyz(
pos.point.x.to_f32(),
(page.size.y - y).to_f32(),
None,
Expand Down