summaryrefslogtreecommitdiff
path: root/crates/typst-pdf
diff options
context:
space:
mode:
authorPgBiel <9021226+PgBiel@users.noreply.github.com>2024-05-10 11:47:02 -0300
committerGitHub <noreply@github.com>2024-05-10 14:47:02 +0000
commit7905de67bcf3ca9b65c076ca02ec4726ba02d22c (patch)
treede9952fe57152796b4524b70b913eb5e6aa67864 /crates/typst-pdf
parentbe12762d942e978ddf2e0ac5c34125264ab483b7 (diff)
Add parameter to select pages to be exported by CLI (#4039)
Diffstat (limited to 'crates/typst-pdf')
-rw-r--r--crates/typst-pdf/src/lib.rs25
-rw-r--r--crates/typst-pdf/src/outline.rs16
-rw-r--r--crates/typst-pdf/src/page.rs82
-rw-r--r--crates/typst-pdf/src/pattern.rs2
4 files changed, 92 insertions, 33 deletions
diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs
index c55abcb0..e618f572 100644
--- a/crates/typst-pdf/src/lib.rs
+++ b/crates/typst-pdf/src/lib.rs
@@ -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};
@@ -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);
@@ -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,
@@ -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.
@@ -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();
@@ -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![],
@@ -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())));
@@ -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();
diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs
index e247c322..e2195bb7 100644
--- a/crates/typst-pdf/src/outline.rs
+++ b/crates/typst-pdf/src/outline.rs
@@ -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);
@@ -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,
diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs
index 621ac91f..1785e98e 100644
--- a/crates/typst-pdf/src/page.rs
+++ b/crates/typst-pdf/src/page.rs
@@ -27,20 +27,40 @@ use typst::visualize::{
/// Construct page objects.
#[typst_macros::time(name = "construct pages")]
pub(crate) fn construct_pages(ctx: &mut PdfContext, pages: &[Page]) {
- for page in pages {
- let (page_ref, mut encoded) = construct_page(ctx, &page.frame);
- encoded.label = page
- .numbering
+ let mut skipped_pages = 0;
+ for (i, page) in pages.iter().enumerate() {
+ if ctx
+ .exported_pages
.as_ref()
- .and_then(|num| PdfPageLabel::generate(num, page.number));
- ctx.page_refs.push(page_ref);
- ctx.pages.push(encoded);
+ .is_some_and(|ranges| !ranges.includes_page_index(i))
+ {
+ // Don't export this page.
+ ctx.pages.push(None);
+ skipped_pages += 1;
+ } else {
+ let mut encoded = construct_page(ctx, &page.frame);
+ encoded.label = page
+ .numbering
+ .as_ref()
+ .and_then(|num| PdfPageLabel::generate(num, page.number))
+ .or_else(|| {
+ // When some pages were ignored from export, we show a page label with
+ // the correct real (not logical) page number.
+ // This is for consistency with normal output when pages have no numbering
+ // and all are exported: the final PDF page numbers always correspond to
+ // the real (not logical) page numbers. Here, the final PDF page number
+ // will differ, but we can at least use labels to indicate what was
+ // the corresponding real page number in the Typst document.
+ (skipped_pages > 0).then(|| PdfPageLabel::arabic(i + 1))
+ });
+ ctx.pages.push(Some(encoded));
+ }
}
}
/// Construct a page object.
#[typst_macros::time(name = "construct page")]
-pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, EncodedPage) {
+pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> EncodedPage {
let page_ref = ctx.alloc.bump();
let size = frame.size();
@@ -60,7 +80,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod
// Encode the page into the content stream.
write_frame(&mut ctx, frame);
- let page = EncodedPage {
+ EncodedPage {
size,
content: deflate_deferred(ctx.content.finish()),
id: page_ref,
@@ -68,21 +88,20 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Encod
links: ctx.links,
label: None,
resources: ctx.resources,
- };
-
- (page_ref, page)
+ }
}
/// Write the page tree.
pub(crate) fn write_page_tree(ctx: &mut PdfContext) {
+ let mut refs = vec![];
for i in 0..ctx.pages.len() {
- write_page(ctx, i);
+ write_page(ctx, i, &mut refs);
}
ctx.pdf
.pages(ctx.page_tree_ref)
- .count(ctx.page_refs.len() as i32)
- .kids(ctx.page_refs.iter().copied());
+ .count(refs.len() as i32)
+ .kids(refs.iter().copied());
}
/// Write the global resource dictionary that will be referenced by all pages.
@@ -170,10 +189,15 @@ pub(crate) fn write_global_resources(ctx: &mut PdfContext) {
}
/// Write a page tree node.
-fn write_page(ctx: &mut PdfContext, i: usize) {
- let page = &ctx.pages[i];
+fn write_page(ctx: &mut PdfContext, i: usize, refs: &mut Vec<Ref>) {
+ let Some(page) = &ctx.pages[i] else {
+ // Page excluded from export.
+ return;
+ };
let content_id = ctx.alloc.bump();
+ refs.push(page.id);
+
let mut page_writer = ctx.pdf.page(page.id);
page_writer.parent(ctx.page_tree_ref);
@@ -225,7 +249,8 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
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) {
+ // Don't add links to non-exported pages.
+ if let Some(Some(page)) = ctx.pages.get(index) {
annotation
.action()
.action_type(ActionType::GoTo)
@@ -244,9 +269,12 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
}
/// Write the page labels.
+/// They are numbered according to the page's final number, considering pages
+/// which were removed from export, and not according to the page's real or
+/// logical number in the initial Typst document.
pub(crate) fn write_page_labels(ctx: &mut PdfContext) -> Vec<(NonZeroUsize, Ref)> {
- // If there is no page labeled, we skip the writing
- if !ctx.pages.iter().any(|p| {
+ // If there is no exported page labeled, we skip the writing
+ if !ctx.pages.iter().filter_map(Option::as_ref).any(|p| {
p.label
.as_ref()
.is_some_and(|l| l.prefix.is_some() || l.style.is_some())
@@ -258,7 +286,8 @@ pub(crate) fn write_page_labels(ctx: &mut PdfContext) -> Vec<(NonZeroUsize, Ref)
let empty_label = PdfPageLabel::default();
let mut prev: Option<&PdfPageLabel> = None;
- for (i, page) in ctx.pages.iter().enumerate() {
+ // Skip non-exported pages for numbering.
+ for (i, page) in ctx.pages.iter().filter_map(Option::as_ref).enumerate() {
let nr = NonZeroUsize::new(1 + i).unwrap();
// If there are pages with empty labels between labeled pages, we must
// write empty PageLabel entries.
@@ -372,6 +401,17 @@ impl PdfPageLabel {
let offset = style.and(NonZeroUsize::new(number));
Some(PdfPageLabel { prefix, style, offset })
}
+
+ /// Creates an arabic page label with the specified page number.
+ /// For example, this will display page label `11` when given the page
+ /// number 11.
+ fn arabic(number: usize) -> PdfPageLabel {
+ PdfPageLabel {
+ prefix: None,
+ style: Some(PdfPageLabelStyle::Arabic),
+ offset: NonZeroUsize::new(number),
+ }
+ }
}
/// Data for an exported page.
diff --git a/crates/typst-pdf/src/pattern.rs b/crates/typst-pdf/src/pattern.rs
index 5d5942bc..211c056c 100644
--- a/crates/typst-pdf/src/pattern.rs
+++ b/crates/typst-pdf/src/pattern.rs
@@ -116,7 +116,7 @@ fn register_pattern(
};
// Render the body.
- let (_, content) = construct_page(ctx.parent, pattern.frame());
+ let content = construct_page(ctx.parent, pattern.frame());
let mut pdf_pattern = PdfPattern {
transform,