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)
}
v => T::cast(v)
.map(Sides::splat)
.map_err(|msg| with_alternative(msg, "dictionary")),
v => T::cast(v).map(Sides::splat).map_err(|msg| {
with_alternative(
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 crate::font::{find_name, FaceId, FontStore};
use crate::frame::{Element, Frame, Group, Text};
use crate::frame::{Destination, Element, Frame, Group, Text};
use crate::geom::{
self, Color, Dir, Em, Geometry, Length, Numeric, Paint, Point, Shape, Size, Stroke,
Transform,
self, Color, Dir, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size,
Stroke, Transform,
};
use crate::image::{Image, ImageId, ImageStore, RasterImage};
use crate::library::text::Lang;
@ -304,13 +304,18 @@ impl<'a> PdfExporter<'a> {
// The page objects (non-root nodes in the page tree).
let mut page_refs = vec![];
let mut languages = HashMap::new();
for page in self.pages {
let mut page_heights = vec![];
for page in &self.pages {
let page_id = self.alloc.bump();
let content_id = self.alloc.bump();
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);
let w = page.size.x.to_f32();
@ -319,14 +324,25 @@ impl<'a> PdfExporter<'a> {
page_writer.contents(content_id);
let mut annotations = page_writer.annotations();
for (url, rect) in page.links {
annotations
.push()
.subtype(AnnotationType::Link)
.rect(rect)
.action()
for (dest, rect) in page.links {
let mut link = annotations.push();
link.subtype(AnnotationType::Link).rect(rect);
match dest {
Destination::Url(uri) => {
link.action()
.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();
@ -403,7 +419,7 @@ struct PageExporter<'a> {
languages: HashMap<Lang, usize>,
bottom: f32,
content: Content,
links: Vec<(String, Rect)>,
links: Vec<(Destination, Rect)>,
state: State,
saves: Vec<State>,
}
@ -412,7 +428,7 @@ struct PageExporter<'a> {
struct Page {
size: Size,
content: Content,
links: Vec<(String, Rect)>,
links: Vec<(Destination, Rect)>,
languages: HashMap<Lang, usize>,
}
@ -445,7 +461,14 @@ impl<'a> PageExporter<'a> {
fn export(mut self, frame: &Frame) -> Page {
// Make the coordinate system start at the top-left.
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_stroke_color_space(ColorSpaceOperand::Named(SRGB));
self.write_frame(frame);
@ -466,7 +489,7 @@ impl<'a> PageExporter<'a> {
Element::Text(ref text) => self.write_text(x, y, text),
Element::Shape(ref shape) => self.write_shape(x, y, shape),
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();
}
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_y = Length::inf();
let mut max_x = -Length::inf();
@ -649,10 +672,11 @@ impl<'a> PageExporter<'a> {
let x1 = min_x.to_f32();
let x2 = max_x.to_f32();
let y1 = self.bottom - max_y.to_f32();
let y2 = self.bottom - min_y.to_f32();
let y1 = max_y.to_f32();
let y2 = min_y.to_f32();
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) {

View File

@ -127,8 +127,8 @@ impl Frame {
}
/// Link the whole frame to a resource.
pub fn link(&mut self, url: EcoString) {
self.push(Point::zero(), Element::Link(url, self.size));
pub fn link(&mut self, dest: Destination) {
self.push(Point::zero(), Element::Link(dest, self.size));
}
}
@ -217,7 +217,7 @@ pub enum Element {
/// An image and its size.
Image(ImageId, Size),
/// A link to an external resource and its trigger region.
Link(EcoString, Size),
Link(Destination, Size),
}
impl Debug for Element {
@ -227,7 +227,7 @@ impl Debug for Element {
Self::Text(text) => write!(f, "{text:?}"),
Self::Shape(shape) => write!(f, "{shape:?}"),
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.
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.
#[derive(Debug, Hash)]
pub struct LinkNode {
/// The url the link points to.
pub url: EcoString,
/// The destination the link points to.
pub dest: Destination,
/// How the link is represented.
pub body: Option<Content>,
}
@ -17,20 +17,36 @@ impl LinkNode {
/// if `auto`.
pub const FILL: Smart<Paint> = Smart::Auto;
/// 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> {
Ok(Content::show(Self {
url: args.expect::<EcoString>("url")?,
body: args.eat()?,
Ok(Content::show({
let dest = args.expect::<Destination>("destination")?;
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 {
fn unguard(&self, sel: Selector) -> ShowNode {
Self {
url: self.url.clone(),
dest: self.dest.clone(),
body: self.body.as_ref().map(|body| body.unguard(sel)),
}
.pack()
@ -38,7 +54,14 @@ impl Show for LinkNode {
fn encode(&self, _: StyleChain) -> 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 {
Some(body) => Value::Content(body.clone()),
None => Value::None,
@ -47,14 +70,16 @@ impl Show for LinkNode {
}
fn realize(&self, _: &mut Context, _: StyleChain) -> TypResult<Content> {
Ok(self.body.clone().unwrap_or_else(|| {
let url = &self.url;
Ok(self.body.clone().unwrap_or_else(|| match &self.dest {
Destination::Url(url) => {
let mut text = url.as_str();
for prefix in ["mailto:", "tel:"] {
text = text.trim_start_matches(prefix);
}
let shorter = text.len() < url.len();
Content::Text(if shorter { text.into() } else { url.clone() })
}
Destination::Internal(_, _) => panic!("missing body"),
}))
}
@ -65,13 +90,19 @@ impl Show for LinkNode {
mut realized: Content,
) -> TypResult<Content> {
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) {
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();
}

View File

@ -122,7 +122,7 @@ impl TextNode {
pub const SMALLCAPS: bool = false;
/// An URL the text should link to.
#[property(skip, referenced)]
pub const LINK: Option<EcoString> = None;
pub const LINK: Option<Destination> = None;
/// Decorative lines.
#[property(skip, fold)]
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
#move(dx: 10pt, image("/res/rhino.png", width: 1cm))
])
---
// Link to page one.
#link((page: 1, x: 10pt, y: 20pt))[Back to the start]