Links, outlines and metadata
- Links:
surface.link(rect, target, contents), where the target is{ type: "uri" },{ type: "page", index }or{ type: "named", name }. A text run takes alinkoption too. Targets are checked when the builder finishes:page-index-out-of-rangeandunknown-destination. - Named destinations:
builder.destination(name, index). - Outline:
builder.outline([...]), with nested entries, colour, bold, italic andopen. - Page labels:
builder.pageLabels([{ start, style }]). Here, roman numerals for the front matter, then decimal from page 3. - Viewer preferences: for example
pageMode: "outlines", which opens the file with the outline showing. - Metadata:
builder.metadata({ title, author, … })writes the Info dictionary and an XMP packet from the same values (Open, edit and save uses it).
// Navigation: URI, page and named-destination links, an outline with a heading, colour and// styles, page labels and the outline shown on open. The TS twin of// crates/lamina/examples/links_and_outlines.rs.import { Builder, Font, type Rect } from "@lamina/pdf";
const TITLES = ["Title page", "Contents", "Chapter 1", "Chapter 2"] as const;
/** The document's bytes. Needs `init({ text: true })`. */export function linksAndOutlines(): Uint8Array { const row = (y: number): Rect => [36, y - 15, 220, 20]; const b = new Builder({ id: new TextEncoder().encode("lamina-ex-links!"), producer: "lamina examples", creationDate: "D:20261030120000Z", }); const font = Font.standard("Helvetica"); b.font(font); b.destination("chapter-2", 3); TITLES.forEach((title, index) => { const page = b.page("A5"); const s = page.surface(); s.text(title, { font, size: 18, at: [36, 60] }); if (index === 0) { s.text("Visit example.com", { font, size: 12, at: [36, 100] }); s.link(row(100), { type: "uri", uri: "https://example.com/" }, "Example"); s.text("Go to chapter 1", { font, size: 12, at: [36, 125] }); s.link(row(125), { type: "page", index: 2 }); s.text("Go to chapter 2 (named)", { font, size: 12, at: [36, 150] }); s.link(row(150), { type: "named", name: "chapter-2" }); } else { s.text("Back to the title page", { font, size: 12, at: [36, 100], fill: [0, 0.2, 0.8], link: { target: { type: "page", index: 0 }, contents: "Title page" }, }); } page.finish(); }); b.outline([ { title: "Title page", target: { type: "page", index: 0 } }, { title: "Contents", target: { type: "page", index: 1 }, color: [0, 0, 0.6] }, { title: "Chapters", open: true, bold: true, children: [ { title: "Chapter 1", target: { type: "page", index: 2 } }, { title: "Chapter 2", target: { type: "named", name: "chapter-2" }, italic: true }, ], }, ]); b.pageLabels([ { start: 0, style: "lowerRoman" }, { start: 2, style: "decimal" }, ]).viewerPreferences({ pageMode: "outlines" }); return b.finish().save();}//! Navigation: four A5 pages with a URI link, links to pages (forward and back) and through a//! named destination, a linked text run, a nested outline (bookmarks) opened in the viewer,//! and page labels i, ii, 1, 2.//!//! ```sh//! cargo run -p lamina --example links_and_outlines -- [out-dir] # writes <out-dir>/links_and_outlines.pdf//! ```
use std::error::Error;use std::path::PathBuf;
use lamina::{ BuildOptions, Builder, CancelToken, Color, Fit, LabelStyle, LinkTarget, OutlineEntry, PageLabel, PageMode, PageSize, PointPt, Pt, RectPt, SaveOptions, Standard14, ViewerPreferences,};
const TITLES: [&str; 4] = ["Title page", "Contents", "Chapter 1", "Chapter 2"];
fn main() -> Result<(), Box<dyn Error>> { let out = PathBuf::from(std::env::args().nth(1).unwrap_or_else(|| "target/examples-out".into())); std::fs::create_dir_all(&out)?; std::fs::write(out.join("links_and_outlines.pdf"), build()?)?; Ok(())}
/// The document's bytes.pub(crate) fn build() -> Result<Vec<u8>, Box<dyn Error>> { let at = PointPt::from_points; let row = |y: f64| RectPt::from_xywh(Pt::new(36.0), Pt::new(y - 15.0), Pt::new(220.0), Pt::new(20.0)); let options = BuildOptions::default() .with_id(*b"lamina-ex-links!") .with_producer("lamina examples") .with_creation_date("D:20261030120000Z".parse()?); let mut b = Builder::new(options); let helvetica = b.standard_font(Standard14::Helvetica)?; b.destination("chapter-2", 3, Fit::Page); for (index, title) in (0_u32..).zip(TITLES) { let mut page = b.page(PageSize::A5); let s = page.surface(); let _ = s.text(title, &helvetica, Pt::new(18.0), at(36.0, 60.0))?; if index == 0 { let _ = s.text("Visit example.com", &helvetica, Pt::new(12.0), at(36.0, 100.0))?; s.link( row(100.0), LinkTarget::Uri("https://example.com/".into()), Some("Example".into()), ); let _ = s.text("Go to chapter 1", &helvetica, Pt::new(12.0), at(36.0, 125.0))?; s.link(row(125.0), LinkTarget::page(2), None); let _ = s.text("Go to chapter 2 (named)", &helvetica, Pt::new(12.0), at(36.0, 150.0))?; s.link(row(150.0), LinkTarget::Named("chapter-2".into()), None); } else { let _ = s .text("Back to the title page", &helvetica, Pt::new(12.0), at(36.0, 100.0))? .fill(Color::rgb(0.0, 0.2, 0.8)) .link(LinkTarget::page(0), Some("Title page".into())); } page.finish()?; } b.outline(vec![ OutlineEntry::new("Title page", LinkTarget::page(0)), OutlineEntry::new("Contents", LinkTarget::page(1)).with_color(0.0, 0.0, 0.6), OutlineEntry::heading("Chapters") .open(true) .bold() .with_child(OutlineEntry::new("Chapter 1", LinkTarget::page(2))) .with_child(OutlineEntry::new("Chapter 2", LinkTarget::Named("chapter-2".into())).italic()), ])?; b.page_labels(vec![ PageLabel::new(0, Some(LabelStyle::LowerRoman)), PageLabel::new(2, Some(LabelStyle::Decimal)), ]) .viewer_preferences(ViewerPreferences::default().with_page_mode(PageMode::Outlines)); let mut doc = b.finish()?; Ok(doc.save(&SaveOptions::new(), &CancelToken::new())?.bytes)}