From 9b5ee82c167c839efc36639ce15d75fe30fed3c7 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Tue, 18 Mar 2025 17:25:41 +0100 Subject: [PATCH] Fix page range issue --- crates/typst-pdf/src/convert.rs | 71 +++++++++++++++++++++++---------- crates/typst-pdf/src/link.rs | 6 +-- crates/typst-pdf/src/outline.rs | 4 +- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs index 8942bc748..d86f9e081 100644 --- a/crates/typst-pdf/src/convert.rs +++ b/crates/typst-pdf/src/convert.rs @@ -52,10 +52,13 @@ pub fn convert( }; let mut document = Document::new_with(settings); + let page_index_converter = PageIndexConverter::new(&typst_document, &options); + let named_destinations = collect_named_destinations(&typst_document, &page_index_converter); let mut gc = GlobalContext::new( typst_document, options, - collect_named_destinations(typst_document, options), + named_destinations, + page_index_converter ); convert_pages(&mut gc, &mut document)?; @@ -68,12 +71,9 @@ pub fn convert( } fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResult<()> { - let mut skipped_pages = 0; - for (i, typst_page) in gc.document.pages.iter().enumerate() { - if gc.page_excluded(i) { + if gc.page_index_converter.pdf_page_index(i).is_none() { // Don't export this page. - skipped_pages += 1; continue; } else { let mut settings = PageSettings::new( @@ -93,7 +93,7 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul // 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(|| PageLabel::arabic(i + 1)) + gc.page_index_converter.has_skipped_pages().then(|| PageLabel::arabic(i + 1)) }) { settings = settings.with_page_label(label); @@ -219,35 +219,31 @@ pub(crate) struct GlobalContext<'a> { /// Options for PDF export. pub(crate) options: &'a PdfOptions<'a>, /// Mapping between locations in the document and named destinations. - pub(crate) loc_to_named: HashMap, + pub(crate) loc_to_names: HashMap, /// The languages used throughout the document. pub(crate) languages: BTreeMap, + pub(crate) page_index_converter: PageIndexConverter } impl<'a> GlobalContext<'a> { pub(crate) fn new( document: &'a PagedDocument, options: &'a PdfOptions, - loc_to_named: HashMap, + loc_to_names: HashMap, + page_index_converter: PageIndexConverter ) -> GlobalContext<'a> { Self { fonts_forward: HashMap::new(), fonts_backward: HashMap::new(), document, options, - loc_to_named, + loc_to_names, image_to_spans: HashMap::new(), image_spans: HashSet::new(), languages: BTreeMap::new(), + page_index_converter } } - - pub(crate) fn page_excluded(&self, page_index: usize) -> bool { - self.options - .page_ranges - .as_ref() - .is_some_and(|ranges| !ranges.includes_page_index(page_index)) - } } pub(crate) fn handle_frame( @@ -556,7 +552,7 @@ fn finish( fn collect_named_destinations( document: &PagedDocument, - options: &PdfOptions, + pic: &PageIndexConverter ) -> HashMap { let mut locs_to_names = HashMap::new(); @@ -582,11 +578,7 @@ fn collect_named_destinations( let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); // Only add named destination if page belonging to the position is exported. - if options - .page_ranges - .as_ref() - .is_some_and(|ranges| !ranges.includes_page_index(index)) - { + if let Some(index) = pic.pdf_page_index(index) { let named = NamedDestination::new( label.resolve().to_string(), XyzDestination::new( @@ -631,3 +623,38 @@ fn get_configuration(options: &PdfOptions) -> SourceResult { Ok(config) } + +pub(crate) struct PageIndexConverter { + page_indices: HashMap, + skipped_pages: usize, +} + +impl PageIndexConverter { + pub fn new(document: &PagedDocument, options: &PdfOptions) -> Self { + let mut page_indices = HashMap::new(); + let mut skipped_pages = 0; + + for i in 0..document.pages.len() { + if options + .page_ranges + .as_ref() + .is_some_and(|ranges| !ranges.includes_page_index(i)) + { + skipped_pages += 1; + } else { + page_indices.insert(i, i - skipped_pages); + } + } + + Self { page_indices, skipped_pages } + } + + pub(crate) fn has_skipped_pages(&self) -> bool { + self.skipped_pages > 0 + } + + /// Get the PDF page index of a page index, if it's not excluded. + pub(crate) fn pdf_page_index(&self, page_index: usize) -> Option { + self.page_indices.get(&page_index).copied() + } +} diff --git a/crates/typst-pdf/src/link.rs b/crates/typst-pdf/src/link.rs index d59cfe227..64cb8f0a2 100644 --- a/crates/typst-pdf/src/link.rs +++ b/crates/typst-pdf/src/link.rs @@ -58,7 +58,7 @@ pub(crate) fn handle_link( } Destination::Position(p) => *p, Destination::Location(loc) => { - if let Some(nd) = gc.loc_to_named.get(loc) { + if let Some(nd) = gc.loc_to_names.get(loc) { // If a named destination has been registered, it's already guaranteed to // not point to an excluded page. fc.push_annotation( @@ -79,13 +79,13 @@ pub(crate) fn handle_link( }; let page_index = pos.page.get() - 1; - if !gc.page_excluded(page_index) { + if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) { fc.push_annotation( LinkAnnotation::new( rect, None, Target::Destination(krilla::destination::Destination::Xyz( - XyzDestination::new(page_index, pos.point.to_krilla()), + XyzDestination::new(index, pos.point.to_krilla()), )), ) .into(), diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index 834476715..7e337b67c 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -127,10 +127,10 @@ impl<'a> HeadingNode<'a> { let pos = gc.document.introspector.position(loc); let page_index = pos.page.get() - 1; - if !gc.page_excluded(page_index) { + if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) { let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); let dest = XyzDestination::new( - page_index, + index, krilla::geom::Point::from_xy(pos.point.x.to_f32(), y.to_f32()), );