mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Fix named destinations (#3411)
This commit is contained in:
parent
8ed1eff7b5
commit
52571dd9ef
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -1616,8 +1616,9 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdf-writer"
|
name = "pdf-writer"
|
||||||
version = "0.9.0"
|
version = "0.9.2"
|
||||||
source = "git+https://github.com/heinenen/pdf-writer?branch=named_destinations#58c6dc1552aa72f5e2c07a37045526fcf365d34a"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "644b654f2de28457bf1e25a4905a76a563d1128a33ce60cf042f721f6818feaf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
@ -71,7 +71,7 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "p
|
|||||||
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
|
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
|
||||||
parking_lot = "0.12.1"
|
parking_lot = "0.12.1"
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
pdf-writer = "0.9"
|
pdf-writer = "0.9.2"
|
||||||
phf = { version = "0.11", features = ["macros"] }
|
phf = { version = "0.11", features = ["macros"] }
|
||||||
pixglyph = "0.3"
|
pixglyph = "0.3"
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
@ -123,9 +123,6 @@ xz2 = "0.1"
|
|||||||
yaml-front-matter = "0.1"
|
yaml-front-matter = "0.1"
|
||||||
zip = { version = "0.6", default-features = false, features = ["deflate"] }
|
zip = { version = "0.6", default-features = false, features = ["deflate"] }
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
pdf-writer = { git = 'https://github.com/heinenen/pdf-writer', branch = "named_destinations" }
|
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
|
|
||||||
|
@ -17,8 +17,10 @@ use std::sync::Arc;
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use pdf_writer::types::Direction;
|
use pdf_writer::types::Direction;
|
||||||
|
use pdf_writer::writers::Destination;
|
||||||
use pdf_writer::{Finish, Name, Pdf, Ref, Str, TextStr};
|
use pdf_writer::{Finish, Name, Pdf, Ref, Str, TextStr};
|
||||||
use typst::foundations::{Datetime, NativeElement};
|
use typst::foundations::{Datetime, Label, NativeElement};
|
||||||
|
use typst::introspection::Location;
|
||||||
use typst::layout::{Abs, Dir, Em, Transform};
|
use typst::layout::{Abs, Dir, Em, Transform};
|
||||||
use typst::model::{Document, HeadingElem};
|
use typst::model::{Document, HeadingElem};
|
||||||
use typst::text::{Font, Lang};
|
use typst::text::{Font, Lang};
|
||||||
@ -60,6 +62,7 @@ pub fn pdf(
|
|||||||
gradient::write_gradients(&mut ctx);
|
gradient::write_gradients(&mut ctx);
|
||||||
extg::write_external_graphics_states(&mut ctx);
|
extg::write_external_graphics_states(&mut ctx);
|
||||||
pattern::write_patterns(&mut ctx);
|
pattern::write_patterns(&mut ctx);
|
||||||
|
write_named_destinations(&mut ctx);
|
||||||
page::write_page_tree(&mut ctx);
|
page::write_page_tree(&mut ctx);
|
||||||
write_catalog(&mut ctx, ident, timestamp);
|
write_catalog(&mut ctx, ident, timestamp);
|
||||||
ctx.pdf.finish()
|
ctx.pdf.finish()
|
||||||
@ -115,6 +118,11 @@ struct PdfContext<'a> {
|
|||||||
pattern_map: Remapper<PdfPattern>,
|
pattern_map: Remapper<PdfPattern>,
|
||||||
/// Deduplicates external graphics states used across the document.
|
/// Deduplicates external graphics states used across the document.
|
||||||
extg_map: Remapper<ExtGState>,
|
extg_map: Remapper<ExtGState>,
|
||||||
|
|
||||||
|
/// A sorted list of all named destinations.
|
||||||
|
dests: Vec<(Label, Ref)>,
|
||||||
|
/// Maps from locations to named destinations that point to them.
|
||||||
|
loc_to_dest: HashMap<Location, Label>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> PdfContext<'a> {
|
impl<'a> PdfContext<'a> {
|
||||||
@ -142,6 +150,8 @@ impl<'a> PdfContext<'a> {
|
|||||||
gradient_map: Remapper::new(),
|
gradient_map: Remapper::new(),
|
||||||
pattern_map: Remapper::new(),
|
pattern_map: Remapper::new(),
|
||||||
extg_map: Remapper::new(),
|
extg_map: Remapper::new(),
|
||||||
|
dests: vec![],
|
||||||
|
loc_to_dest: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,20 +262,18 @@ fn write_catalog(ctx: &mut PdfContext, ident: Option<&str>, timestamp: Option<Da
|
|||||||
.pair(Name(b"Type"), Name(b"Metadata"))
|
.pair(Name(b"Type"), Name(b"Metadata"))
|
||||||
.pair(Name(b"Subtype"), Name(b"XML"));
|
.pair(Name(b"Subtype"), Name(b"XML"));
|
||||||
|
|
||||||
let destinations = write_and_collect_destinations(ctx);
|
|
||||||
|
|
||||||
// Write the document catalog.
|
// Write the document catalog.
|
||||||
let mut catalog = ctx.pdf.catalog(ctx.alloc.bump());
|
let mut catalog = ctx.pdf.catalog(ctx.alloc.bump());
|
||||||
catalog.pages(ctx.page_tree_ref);
|
catalog.pages(ctx.page_tree_ref);
|
||||||
catalog.viewer_preferences().direction(dir);
|
catalog.viewer_preferences().direction(dir);
|
||||||
catalog.metadata(meta_ref);
|
catalog.metadata(meta_ref);
|
||||||
|
|
||||||
// Write the named destinations.
|
// Write the named destination tree.
|
||||||
let mut name_dict = catalog.names();
|
let mut name_dict = catalog.names();
|
||||||
let mut dests_name_tree = name_dict.destinations();
|
let mut dests_name_tree = name_dict.destinations();
|
||||||
let mut names = dests_name_tree.names();
|
let mut names = dests_name_tree.names();
|
||||||
for (name, dest_ref, _page_ref, _x, _y) in destinations {
|
for &(name, dest_ref, ..) in &ctx.dests {
|
||||||
names.insert(name, dest_ref);
|
names.insert(Str(name.as_str().as_bytes()), dest_ref);
|
||||||
}
|
}
|
||||||
names.finish();
|
names.finish();
|
||||||
dests_name_tree.finish();
|
dests_name_tree.finish();
|
||||||
@ -291,38 +299,43 @@ fn write_catalog(ctx: &mut PdfContext, ident: Option<&str>, timestamp: Option<Da
|
|||||||
catalog.finish();
|
catalog.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_and_collect_destinations<'a>(
|
/// Fills in the map and vector for named destinations and writes the indirect
|
||||||
ctx: &mut PdfContext,
|
/// destination objects.
|
||||||
) -> Vec<(Str<'a>, Ref, Ref, f32, f32)> {
|
fn write_named_destinations(ctx: &mut PdfContext) {
|
||||||
let mut destinations = vec![];
|
let mut seen = HashSet::new();
|
||||||
|
|
||||||
let mut seen_labels = HashSet::new();
|
// Find all headings that have a label and are the first among other
|
||||||
let elements = ctx.document.introspector.query(&HeadingElem::elem().select());
|
// headings with the same label.
|
||||||
for elem in elements.iter() {
|
let mut matches: Vec<_> = ctx
|
||||||
let heading = elem.to_packed::<HeadingElem>().unwrap();
|
.document
|
||||||
if let Some(label) = heading.label() {
|
.introspector
|
||||||
if !seen_labels.contains(&label) {
|
.query(&HeadingElem::elem().select())
|
||||||
let loc = heading.location().unwrap();
|
.iter()
|
||||||
let name = Str(label.as_str().as_bytes());
|
.filter_map(|elem| elem.location().zip(elem.label()))
|
||||||
let pos = ctx.document.introspector.position(loc);
|
.filter(|&(_, label)| seen.insert(label))
|
||||||
let index = pos.page.get() - 1;
|
.collect();
|
||||||
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
|
|
||||||
if let Some(page) = ctx.pages.get(index) {
|
// Named destinations must be sorted by key.
|
||||||
seen_labels.insert(label);
|
matches.sort_by_key(|&(_, label)| label);
|
||||||
let page_ref = ctx.page_refs[index];
|
|
||||||
let x = pos.point.x.to_f32();
|
for (loc, label) in matches {
|
||||||
let y = (page.size.y - y).to_f32();
|
let pos = ctx.document.introspector.position(loc);
|
||||||
let dest_ref = ctx.alloc.bump();
|
let index = pos.page.get() - 1;
|
||||||
destinations.push((name, dest_ref, page_ref, x, y))
|
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
|
||||||
}
|
|
||||||
}
|
if let 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();
|
||||||
|
ctx.dests.push((label, dest_ref));
|
||||||
|
ctx.loc_to_dest.insert(loc, label);
|
||||||
|
ctx.pdf
|
||||||
|
.indirect(dest_ref)
|
||||||
|
.start::<Destination>()
|
||||||
|
.page(page.id)
|
||||||
|
.xyz(x, y, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
destinations.sort_by_key(|i| i.0);
|
|
||||||
for (_name, dest_ref, page_ref, x, y) in destinations.iter().copied() {
|
|
||||||
ctx.pdf.destination(dest_ref).page(page_ref).xyz(x, y, None);
|
|
||||||
}
|
|
||||||
destinations
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compress data with the DEFLATE algorithm.
|
/// Compress data with the DEFLATE algorithm.
|
||||||
|
@ -8,12 +8,11 @@ use pdf_writer::types::{
|
|||||||
};
|
};
|
||||||
use pdf_writer::writers::PageLabel;
|
use pdf_writer::writers::PageLabel;
|
||||||
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str, TextStr};
|
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str, TextStr};
|
||||||
use typst::foundations::{NativeElement, Selector};
|
use typst::introspection::Meta;
|
||||||
use typst::introspection::{Location, Meta};
|
|
||||||
use typst::layout::{
|
use typst::layout::{
|
||||||
Abs, Em, Frame, FrameItem, GroupItem, Page, Point, Ratio, Size, Transform,
|
Abs, Em, Frame, FrameItem, GroupItem, Page, Point, Ratio, Size, Transform,
|
||||||
};
|
};
|
||||||
use typst::model::{Destination, Document, HeadingElem, Numbering};
|
use typst::model::{Destination, Numbering};
|
||||||
use typst::text::{Case, Font, TextItem};
|
use typst::text::{Case, Font, TextItem};
|
||||||
use typst::util::{Deferred, Numeric};
|
use typst::util::{Deferred, Numeric};
|
||||||
use typst::visualize::{
|
use typst::visualize::{
|
||||||
@ -142,16 +141,6 @@ pub(crate) fn write_page_tree(ctx: &mut PdfContext) {
|
|||||||
ctx.colors.write_functions(&mut ctx.pdf);
|
ctx.colors.write_functions(&mut ctx.pdf);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name_from_loc<'a>(doc: &Document, loc: &Location) -> Option<Name<'a>> {
|
|
||||||
let elem = doc.introspector.query_first(&Selector::Location(*loc))?;
|
|
||||||
let label = elem.label()?;
|
|
||||||
debug_assert!(doc.introspector.query_label(label).is_ok());
|
|
||||||
if elem.elem() != HeadingElem::elem() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(Name(label.as_str().as_bytes()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a page tree node.
|
/// Write a page tree node.
|
||||||
fn write_page(ctx: &mut PdfContext, i: usize) {
|
fn write_page(ctx: &mut PdfContext, i: usize) {
|
||||||
let page = &ctx.pages[i];
|
let page = &ctx.pages[i];
|
||||||
@ -191,11 +180,12 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
|
|||||||
}
|
}
|
||||||
Destination::Position(pos) => *pos,
|
Destination::Position(pos) => *pos,
|
||||||
Destination::Location(loc) => {
|
Destination::Location(loc) => {
|
||||||
if let Some(name) = name_from_loc(ctx.document, loc) {
|
if let Some(key) = ctx.loc_to_dest.get(loc) {
|
||||||
annotation
|
annotation
|
||||||
.action()
|
.action()
|
||||||
.action_type(ActionType::GoTo)
|
.action_type(ActionType::GoTo)
|
||||||
.destination_named(name);
|
// `key` must be a `Str`, not a `Name`.
|
||||||
|
.pair(Name(b"D"), Str(key.as_str().as_bytes()));
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
ctx.document.introspector.position(*loc)
|
ctx.document.introspector.position(*loc)
|
||||||
@ -205,12 +195,13 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
|
|||||||
|
|
||||||
let index = pos.page.get() - 1;
|
let index = pos.page.get() - 1;
|
||||||
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
|
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
|
||||||
|
|
||||||
if let Some(page) = ctx.pages.get(index) {
|
if let Some(page) = ctx.pages.get(index) {
|
||||||
annotation
|
annotation
|
||||||
.action()
|
.action()
|
||||||
.action_type(ActionType::GoTo)
|
.action_type(ActionType::GoTo)
|
||||||
.destination()
|
.destination()
|
||||||
.page(ctx.page_refs[index])
|
.page(page.id)
|
||||||
.xyz(pos.point.x.to_f32(), (page.size.y - y).to_f32(), None);
|
.xyz(pos.point.x.to_f32(), (page.size.y - y).to_f32(), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
@ -23,7 +24,7 @@ struct Interner {
|
|||||||
/// slow to look up a string in the interner, so we want to avoid doing it
|
/// slow to look up a string in the interner, so we want to avoid doing it
|
||||||
/// unnecessarily. For this reason, the user should use the [`PicoStr::resolve`]
|
/// unnecessarily. For this reason, the user should use the [`PicoStr::resolve`]
|
||||||
/// method to get the underlying string, such that the lookup is done only once.
|
/// method to get the underlying string, such that the lookup is done only once.
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct PicoStr(u32);
|
pub struct PicoStr(u32);
|
||||||
|
|
||||||
impl PicoStr {
|
impl PicoStr {
|
||||||
@ -63,6 +64,18 @@ impl Debug for PicoStr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Ord for PicoStr {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.resolve().cmp(other.resolve())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for PicoStr {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AsRef<str> for PicoStr {
|
impl AsRef<str> for PicoStr {
|
||||||
fn as_ref(&self) -> &str {
|
fn as_ref(&self) -> &str {
|
||||||
self.resolve()
|
self.resolve()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user