summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/eval/value.rs10
-rw-r--r--src/export/pdf.rs70
-rw-r--r--src/frame.rs28
-rw-r--r--src/library/text/link.rs65
-rw-r--r--src/library/text/mod.rs2
-rw-r--r--tests/ref/text/link.pngbin35262 -> 41124 bytes
-rw-r--r--tests/typ/text/link.typ4
7 files changed, 131 insertions, 48 deletions
diff --git a/src/eval/value.rs b/src/eval/value.rs
index 011a7613..b47d1e91 100644
--- a/src/eval/value.rs
+++ b/src/eval/value.rs
@@ -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",
+ )
+ }),
}
}
}
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index 7fda504e..aeb5c47e 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -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 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);
+ 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()
- .action_type(ActionType::Uri)
- .uri(Str(url.as_bytes()));
+ 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(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) {
diff --git a/src/frame.rs b/src/frame.rs
index 04551e6e..6475f92a 100644
--- a/src/frame.rs
+++ b/src/frame.rs
@@ -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),
+ }
+ }
+}
diff --git a/src/library/text/link.rs b/src/library/text/link.rs
index 728b594f..2ce7a469 100644
--- a/src/library/text/link.rs
+++ b/src/library/text/link.rs
@@ -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;
- let mut text = url.as_str();
- for prefix in ["mailto:", "tel:"] {
- text = text.trim_start_matches(prefix);
+ 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() })
}
- 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();
}
diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs
index bbe397ca..a1089486 100644
--- a/src/library/text/mod.rs
+++ b/src/library/text/mod.rs
@@ -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![];
diff --git a/tests/ref/text/link.png b/tests/ref/text/link.png
index 75ea19e8..5a9b4ca3 100644
--- a/tests/ref/text/link.png
+++ b/tests/ref/text/link.png
Binary files differ
diff --git a/tests/typ/text/link.typ b/tests/typ/text/link.typ
index 64697bff..01c0b88d 100644
--- a/tests/typ/text/link.typ
+++ b/tests/typ/text/link.typ
@@ -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]