Intra-document links

This commit is contained in:
Martin Haug 2022-05-27 12:56:20 +02:00
parent 0170913d54
commit 99cb655832
7 changed files with 131 additions and 48 deletions

View File

@ -634,9 +634,13 @@ where
Ok(sides) Ok(sides)
} }
v => T::cast(v) v => T::cast(v).map(Sides::splat).map_err(|msg| {
.map(Sides::splat) with_alternative(
.map_err(|msg| with_alternative(msg, "dictionary")), msg,
"dictionary with any of `left`, `top`, `right`, `bottom`,\
`x`, `y`, or `rest` as keys",
)
}),
} }
} }
} }

View File

@ -16,10 +16,10 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset::subset; use super::subset::subset;
use crate::font::{find_name, FaceId, FontStore}; use crate::font::{find_name, FaceId, FontStore};
use crate::frame::{Element, Frame, Group, Text}; use crate::frame::{Destination, Element, Frame, Group, Text};
use crate::geom::{ use crate::geom::{
self, Color, Dir, Em, Geometry, Length, Numeric, Paint, Point, Shape, Size, Stroke, self, Color, Dir, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size,
Transform, Stroke, Transform,
}; };
use crate::image::{Image, ImageId, ImageStore, RasterImage}; use crate::image::{Image, ImageId, ImageStore, RasterImage};
use crate::library::text::Lang; use crate::library::text::Lang;
@ -304,13 +304,18 @@ impl<'a> PdfExporter<'a> {
// The page objects (non-root nodes in the page tree). // The page objects (non-root nodes in the page tree).
let mut page_refs = vec![]; let mut page_refs = vec![];
let mut languages = HashMap::new(); let mut page_heights = vec![];
for page in self.pages { for page in &self.pages {
let page_id = self.alloc.bump(); let page_id = self.alloc.bump();
let content_id = self.alloc.bump();
page_refs.push(page_id); page_refs.push(page_id);
page_heights.push(page.size.y.to_f32());
}
let mut page_writer = self.writer.page(page_id); let mut languages = HashMap::new();
for (page, page_id) in self.pages.into_iter().zip(page_refs.iter()) {
let content_id = self.alloc.bump();
let mut page_writer = self.writer.page(*page_id);
page_writer.parent(page_tree_ref); page_writer.parent(page_tree_ref);
let w = page.size.x.to_f32(); let w = page.size.x.to_f32();
@ -319,14 +324,25 @@ impl<'a> PdfExporter<'a> {
page_writer.contents(content_id); page_writer.contents(content_id);
let mut annotations = page_writer.annotations(); let mut annotations = page_writer.annotations();
for (url, rect) in page.links { for (dest, rect) in page.links {
annotations let mut link = annotations.push();
.push() link.subtype(AnnotationType::Link).rect(rect);
.subtype(AnnotationType::Link) match dest {
.rect(rect) Destination::Url(uri) => {
.action() link.action()
.action_type(ActionType::Uri) .action_type(ActionType::Uri)
.uri(Str(url.as_bytes())); .uri(Str(uri.as_str().as_bytes()));
}
Destination::Internal(page, point) => {
let page = page - 1;
let height = page_heights[page];
link.action()
.action_type(ActionType::GoTo)
.destination_direct()
.page(page_refs[page])
.xyz(point.x.to_f32(), height - point.y.to_f32(), None);
}
}
} }
annotations.finish(); annotations.finish();
@ -403,7 +419,7 @@ struct PageExporter<'a> {
languages: HashMap<Lang, usize>, languages: HashMap<Lang, usize>,
bottom: f32, bottom: f32,
content: Content, content: Content,
links: Vec<(String, Rect)>, links: Vec<(Destination, Rect)>,
state: State, state: State,
saves: Vec<State>, saves: Vec<State>,
} }
@ -412,7 +428,7 @@ struct PageExporter<'a> {
struct Page { struct Page {
size: Size, size: Size,
content: Content, content: Content,
links: Vec<(String, Rect)>, links: Vec<(Destination, Rect)>,
languages: HashMap<Lang, usize>, languages: HashMap<Lang, usize>,
} }
@ -445,7 +461,14 @@ impl<'a> PageExporter<'a> {
fn export(mut self, frame: &Frame) -> Page { fn export(mut self, frame: &Frame) -> Page {
// Make the coordinate system start at the top-left. // Make the coordinate system start at the top-left.
self.bottom = frame.size.y.to_f32(); self.bottom = frame.size.y.to_f32();
self.content.transform([1.0, 0.0, 0.0, -1.0, 0.0, self.bottom]); self.transform(Transform {
sx: Ratio::one(),
ky: Ratio::zero(),
kx: Ratio::zero(),
sy: Ratio::new(-1.0),
tx: Length::zero(),
ty: frame.size.y,
});
self.content.set_fill_color_space(ColorSpaceOperand::Named(SRGB)); self.content.set_fill_color_space(ColorSpaceOperand::Named(SRGB));
self.content.set_stroke_color_space(ColorSpaceOperand::Named(SRGB)); self.content.set_stroke_color_space(ColorSpaceOperand::Named(SRGB));
self.write_frame(frame); self.write_frame(frame);
@ -466,7 +489,7 @@ impl<'a> PageExporter<'a> {
Element::Text(ref text) => self.write_text(x, y, text), Element::Text(ref text) => self.write_text(x, y, text),
Element::Shape(ref shape) => self.write_shape(x, y, shape), Element::Shape(ref shape) => self.write_shape(x, y, shape),
Element::Image(id, size) => self.write_image(x, y, id, size), Element::Image(id, size) => self.write_image(x, y, id, size),
Element::Link(ref url, size) => self.write_link(pos, url, size), Element::Link(ref dest, size) => self.write_link(pos, dest, size),
} }
} }
} }
@ -627,7 +650,7 @@ impl<'a> PageExporter<'a> {
self.content.restore_state(); self.content.restore_state();
} }
fn write_link(&mut self, pos: Point, url: &str, size: Size) { fn write_link(&mut self, pos: Point, dest: &Destination, size: Size) {
let mut min_x = Length::inf(); let mut min_x = Length::inf();
let mut min_y = Length::inf(); let mut min_y = Length::inf();
let mut max_x = -Length::inf(); let mut max_x = -Length::inf();
@ -649,10 +672,11 @@ impl<'a> PageExporter<'a> {
let x1 = min_x.to_f32(); let x1 = min_x.to_f32();
let x2 = max_x.to_f32(); let x2 = max_x.to_f32();
let y1 = self.bottom - max_y.to_f32(); let y1 = max_y.to_f32();
let y2 = self.bottom - min_y.to_f32(); let y2 = min_y.to_f32();
let rect = Rect::new(x1, y1, x2, y2); let rect = Rect::new(x1, y1, x2, y2);
self.links.push((url.to_string(), rect));
self.links.push((dest.clone(), rect));
} }
fn save_state(&mut self) { fn save_state(&mut self) {

View File

@ -127,8 +127,8 @@ impl Frame {
} }
/// Link the whole frame to a resource. /// Link the whole frame to a resource.
pub fn link(&mut self, url: EcoString) { pub fn link(&mut self, dest: Destination) {
self.push(Point::zero(), Element::Link(url, self.size)); self.push(Point::zero(), Element::Link(dest, self.size));
} }
} }
@ -217,7 +217,7 @@ pub enum Element {
/// An image and its size. /// An image and its size.
Image(ImageId, Size), Image(ImageId, Size),
/// A link to an external resource and its trigger region. /// A link to an external resource and its trigger region.
Link(EcoString, Size), Link(Destination, Size),
} }
impl Debug for Element { impl Debug for Element {
@ -227,7 +227,7 @@ impl Debug for Element {
Self::Text(text) => write!(f, "{text:?}"), Self::Text(text) => write!(f, "{text:?}"),
Self::Shape(shape) => write!(f, "{shape:?}"), Self::Shape(shape) => write!(f, "{shape:?}"),
Self::Image(image, _) => write!(f, "{image:?}"), Self::Image(image, _) => write!(f, "{image:?}"),
Self::Link(url, _) => write!(f, "Link({url:?})"), Self::Link(target, _) => write!(f, "Link({target:?})"),
} }
} }
} }
@ -308,3 +308,23 @@ pub struct Glyph {
/// The first character of the glyph's cluster. /// The first character of the glyph's cluster.
pub c: char, pub c: char,
} }
/// A link destination.
#[derive(Clone, Eq, PartialEq, Hash)]
pub enum Destination {
/// A link to a point on a page.
Internal(usize, Point),
/// A link to a URL.
Url(EcoString),
}
impl Debug for Destination {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Internal(page, point) => {
write!(f, "Internal(Page {}, {:?})", page, point)
}
Self::Url(url) => write!(f, "Url({})", url),
}
}
}

View File

@ -5,8 +5,8 @@ use crate::util::EcoString;
/// Link text and other elements to an URL. /// Link text and other elements to an URL.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct LinkNode { pub struct LinkNode {
/// The url the link points to. /// The destination the link points to.
pub url: EcoString, pub dest: Destination,
/// How the link is represented. /// How the link is represented.
pub body: Option<Content>, pub body: Option<Content>,
} }
@ -17,20 +17,36 @@ impl LinkNode {
/// if `auto`. /// if `auto`.
pub const FILL: Smart<Paint> = Smart::Auto; pub const FILL: Smart<Paint> = Smart::Auto;
/// Whether to underline link. /// Whether to underline link.
pub const UNDERLINE: bool = true; pub const UNDERLINE: Smart<bool> = Smart::Auto;
fn construct(_: &mut Machine, args: &mut Args) -> TypResult<Content> { fn construct(_: &mut Machine, args: &mut Args) -> TypResult<Content> {
Ok(Content::show(Self { Ok(Content::show({
url: args.expect::<EcoString>("url")?, let dest = args.expect::<Destination>("destination")?;
body: args.eat()?, let body = match dest {
Destination::Url(_) => args.eat()?,
Destination::Internal(_, _) => Some(args.expect("body")?),
};
Self { dest, body }
})) }))
} }
} }
castable! {
Destination,
Expected: "string or dictionary with `page`, `x`, and `y` keys",
Value::Str(string) => Self::Url(string),
Value::Dict(dict) => {
let page: i64 = dict.get(&EcoString::from_str("page"))?.clone().cast()?;
let x: RawLength = dict.get(&EcoString::from_str("x"))?.clone().cast()?;
let y: RawLength = dict.get(&EcoString::from_str("y"))?.clone().cast()?;
Self::Internal(page as usize, Point::new(x.length, y.length))
},
}
impl Show for LinkNode { impl Show for LinkNode {
fn unguard(&self, sel: Selector) -> ShowNode { fn unguard(&self, sel: Selector) -> ShowNode {
Self { Self {
url: self.url.clone(), dest: self.dest.clone(),
body: self.body.as_ref().map(|body| body.unguard(sel)), body: self.body.as_ref().map(|body| body.unguard(sel)),
} }
.pack() .pack()
@ -38,7 +54,14 @@ impl Show for LinkNode {
fn encode(&self, _: StyleChain) -> Dict { fn encode(&self, _: StyleChain) -> Dict {
dict! { dict! {
"url" => Value::Str(self.url.clone()), "url" => match &self.dest {
Destination::Url(url) => Value::Str(url.clone()),
Destination::Internal(page, point) => Value::Dict(dict!{
"page" => Value::Int(*page as i64),
"x" => Value::Length(point.x.into()),
"y" => Value::Length(point.y.into()),
}),
},
"body" => match &self.body { "body" => match &self.body {
Some(body) => Value::Content(body.clone()), Some(body) => Value::Content(body.clone()),
None => Value::None, None => Value::None,
@ -47,14 +70,16 @@ impl Show for LinkNode {
} }
fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult<Content> { fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult<Content> {
Ok(self.body.clone().unwrap_or_else(|| { Ok(self.body.clone().unwrap_or_else(|| match &self.dest {
let url = &self.url; Destination::Url(url) => {
let mut text = url.as_str(); let mut text = url.as_str();
for prefix in ["mailto:", "tel:"] { for prefix in ["mailto:", "tel:"] {
text = text.trim_start_matches(prefix); text = text.trim_start_matches(prefix);
}
let shorter = text.len() < url.len();
Content::Text(if shorter { text.into() } else { url.clone() })
} }
let shorter = text.len() < url.len(); Destination::Internal(_, _) => panic!("missing body"),
Content::Text(if shorter { text.into() } else { url.clone() })
})) }))
} }
@ -65,13 +90,19 @@ impl Show for LinkNode {
mut realized: Content, mut realized: Content,
) -> TypResult<Content> { ) -> TypResult<Content> {
let mut map = StyleMap::new(); let mut map = StyleMap::new();
map.set(TextNode::LINK, Some(self.url.clone())); map.set(TextNode::LINK, Some(self.dest.clone()));
if let Smart::Custom(fill) = styles.get(Self::FILL) { if let Smart::Custom(fill) = styles.get(Self::FILL) {
map.set(TextNode::FILL, fill); map.set(TextNode::FILL, fill);
} }
if styles.get(Self::UNDERLINE) { if match styles.get(Self::UNDERLINE) {
Smart::Auto => match &self.dest {
Destination::Url(_) => true,
Destination::Internal(_, _) => false,
},
Smart::Custom(underline) => underline,
} {
realized = realized.underlined(); realized = realized.underlined();
} }

View File

@ -122,7 +122,7 @@ impl TextNode {
pub const SMALLCAPS: bool = false; pub const SMALLCAPS: bool = false;
/// An URL the text should link to. /// An URL the text should link to.
#[property(skip, referenced)] #[property(skip, referenced)]
pub const LINK: Option<EcoString> = None; pub const LINK: Option<Destination> = None;
/// Decorative lines. /// Decorative lines.
#[property(skip, fold)] #[property(skip, fold)]
pub const DECO: Decoration = vec![]; pub const DECO: Decoration = vec![];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -33,3 +33,7 @@ My cool #move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink)))
My cool rhino My cool rhino
#move(dx: 10pt, image("/res/rhino.png", width: 1cm)) #move(dx: 10pt, image("/res/rhino.png", width: 1cm))
]) ])
---
// Link to page one.
#link((page: 1, x: 10pt, y: 20pt))[Back to the start]