summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-06-01 11:47:05 +0200
committerGitHub <noreply@github.com>2022-06-01 11:47:05 +0200
commit665ed12825918bd02a6d6dbcb67860a83dd41600 (patch)
tree65d700531cd8a47fab4789c8feadaac08953b096 /src
parent97858e5992a52459dd8a34be7a6b4786952b491a (diff)
parent755d46819894f9ad5285526206892471e6e71ef8 (diff)
Merge pull request #72 from typst/true-superscript
Sub- and superscripts
Diffstat (limited to 'src')
-rw-r--r--src/export/pdf.rs30
-rw-r--r--src/library/mod.rs2
-rw-r--r--src/library/text/deco.rs3
-rw-r--r--src/library/text/mod.rs7
-rw-r--r--src/library/text/par.rs8
-rw-r--r--src/library/text/shaping.rs38
-rw-r--r--src/library/text/shift.rs178
7 files changed, 230 insertions, 36 deletions
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index 46f3d389..aa7acd41 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -521,17 +521,18 @@ impl<'a> PageExporter<'a> {
}
fn write_text(&mut self, x: f32, y: f32, text: &Text) {
+ *self.languages.entry(text.lang).or_insert(0) += text.glyphs.len();
self.glyphs
.entry(text.face_id)
.or_default()
.extend(text.glyphs.iter().map(|g| g.id));
- self.content.begin_text();
- self.set_font(text.face_id, text.size);
- self.set_fill(text.fill);
-
let face = self.fonts.get(text.face_id);
+ self.set_fill(text.fill);
+ self.set_font(text.face_id, text.size);
+ self.content.begin_text();
+
// Position the text.
self.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]);
@@ -568,11 +569,6 @@ impl<'a> PageExporter<'a> {
items.show(Str(&encoded));
}
- self.languages
- .entry(text.lang)
- .and_modify(|x| *x += text.glyphs.len())
- .or_insert_with(|| text.glyphs.len());
-
items.finish();
positioned.finish();
self.content.end_text();
@@ -583,6 +579,14 @@ impl<'a> PageExporter<'a> {
return;
}
+ if let Some(fill) = shape.fill {
+ self.set_fill(fill);
+ }
+
+ if let Some(stroke) = shape.stroke {
+ self.set_stroke(stroke);
+ }
+
match shape.geometry {
Geometry::Rect(size) => {
let w = size.x.to_f32();
@@ -606,14 +610,6 @@ impl<'a> PageExporter<'a> {
}
}
- if let Some(fill) = shape.fill {
- self.set_fill(fill);
- }
-
- if let Some(stroke) = shape.stroke {
- self.set_stroke(stroke);
- }
-
match (shape.fill, shape.stroke) {
(None, None) => unreachable!(),
(Some(_), None) => self.content.fill_nonzero(),
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 9708475b..d78e38ca 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -28,6 +28,8 @@ pub fn new() -> Scope {
std.def_node::<text::UnderlineNode>("underline");
std.def_node::<text::StrikethroughNode>("strike");
std.def_node::<text::OverlineNode>("overline");
+ std.def_node::<text::SuperNode>("super");
+ std.def_node::<text::SubNode>("sub");
std.def_node::<text::LinkNode>("link");
std.def_node::<text::RepeatNode>("repeat");
std.def_fn("lower", text::lower);
diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs
index cec4ca9e..c5217e8e 100644
--- a/src/library/text/deco.rs
+++ b/src/library/text/deco.rs
@@ -90,6 +90,7 @@ pub fn decorate(
deco: &Decoration,
fonts: &FontStore,
text: &Text,
+ shift: Length,
pos: Point,
width: Length,
) {
@@ -102,7 +103,7 @@ pub fn decorate(
};
let evade = deco.evade && deco.line != STRIKETHROUGH;
- let offset = deco.offset.unwrap_or(-metrics.position.at(text.size));
+ let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)) - shift;
let stroke = deco.stroke.unwrap_or(Stroke {
paint: text.fill,
thickness: metrics.thickness.at(text.size),
diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs
index a1089486..be20b3ef 100644
--- a/src/library/text/mod.rs
+++ b/src/library/text/mod.rs
@@ -8,6 +8,7 @@ mod quotes;
mod raw;
mod repeat;
mod shaping;
+mod shift;
pub use deco::*;
pub use lang::*;
@@ -17,6 +18,7 @@ pub use quotes::*;
pub use raw::*;
pub use repeat::*;
pub use shaping::*;
+pub use shift::*;
use std::borrow::Cow;
@@ -60,6 +62,9 @@ impl TextNode {
/// The width of spaces relative to the default space width.
#[property(resolve)]
pub const SPACING: Relative<RawLength> = Relative::one();
+ /// The offset of the baseline.
+ #[property(resolve)]
+ pub const BASELINE: RawLength = RawLength::zero();
/// Whether glyphs can hang over into the margin.
pub const OVERHANG: bool = true;
/// The top end of the text bounding box.
@@ -98,8 +103,6 @@ impl TextNode {
pub const NUMBER_TYPE: Smart<NumberType> = Smart::Auto;
/// The width of numbers / figures.
pub const NUMBER_WIDTH: Smart<NumberWidth> = Smart::Auto;
- /// How to position numbers.
- pub const NUMBER_POSITION: NumberPosition = NumberPosition::Normal;
/// Whether to have a slash through the zero glyph. ("zero")
pub const SLASHED_ZERO: bool = false;
/// Whether to convert fractions. ("frac")
diff --git a/src/library/text/par.rs b/src/library/text/par.rs
index a6f7c273..695d8066 100644
--- a/src/library/text/par.rs
+++ b/src/library/text/par.rs
@@ -551,7 +551,13 @@ fn prepare<'a>(
} else {
let size = Size::new(regions.first.x, regions.base.y);
let pod = Regions::one(size, regions.base, Spec::splat(false));
- let frame = node.layout(ctx, &pod, styles)?.remove(0);
+ let mut frame = node.layout(ctx, &pod, styles)?.remove(0);
+ let shift = styles.get(TextNode::BASELINE);
+
+ if !shift.is_zero() {
+ Arc::make_mut(&mut frame).translate(Point::with_y(shift));
+ }
+
items.push(Item::Frame(frame));
}
}
diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs
index 68499a01..1f3d2f55 100644
--- a/src/library/text/shaping.rs
+++ b/src/library/text/shaping.rs
@@ -41,6 +41,8 @@ pub struct ShapedGlyph {
pub x_advance: Em,
/// The horizontal offset of the glyph.
pub x_offset: Em,
+ /// The vertical offset of the glyph.
+ pub y_offset: Em,
/// A value that is the same for all glyphs belong to one cluster.
pub cluster: usize,
/// Whether splitting the shaping result before this glyph would yield the
@@ -84,10 +86,17 @@ impl<'a> ShapedText<'a> {
let mut frame = Frame::new(size);
frame.baseline = Some(top);
- for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
- let pos = Point::new(offset, top);
+ let shift = self.styles.get(TextNode::BASELINE);
+ let lang = self.styles.get(TextNode::LANG);
+ let decos = self.styles.get(TextNode::DECO);
+ let fill = self.styles.get(TextNode::FILL);
+ let link = self.styles.get(TextNode::LINK);
+
+ for ((face_id, y_offset), group) in
+ self.glyphs.as_ref().group_by_key(|g| (g.face_id, g.y_offset))
+ {
+ let pos = Point::new(offset, top + shift + y_offset.at(self.size));
- let fill = self.styles.get(TextNode::FILL);
let glyphs = group
.iter()
.map(|glyph| Glyph {
@@ -107,7 +116,7 @@ impl<'a> ShapedText<'a> {
let text = Text {
face_id,
size: self.size,
- lang: self.styles.get(TextNode::LANG),
+ lang,
fill,
glyphs,
};
@@ -115,8 +124,8 @@ impl<'a> ShapedText<'a> {
let width = text.width();
// Apply line decorations.
- for deco in self.styles.get(TextNode::DECO) {
- decorate(&mut frame, &deco, fonts, &text, pos, width);
+ for deco in &decos {
+ decorate(&mut frame, &deco, fonts, &text, shift, pos, width);
}
frame.insert(text_layer, pos, Element::Text(text));
@@ -124,8 +133,8 @@ impl<'a> ShapedText<'a> {
}
// Apply link if it exists.
- if let Some(url) = self.styles.get(TextNode::LINK) {
- frame.link(url.clone());
+ if let Some(dest) = link {
+ frame.link(dest.clone());
}
frame
@@ -212,11 +221,14 @@ impl<'a> ShapedText<'a> {
let x_advance = face.to_em(ttf.glyph_hor_advance(glyph_id)?);
let cluster = self.glyphs.last().map(|g| g.cluster).unwrap_or_default();
self.width += x_advance.at(self.size);
+ let baseline_shift = self.styles.get(TextNode::BASELINE);
+
self.glyphs.to_mut().push(ShapedGlyph {
face_id,
glyph_id: glyph_id.0,
x_advance,
x_offset: Em::zero(),
+ y_offset: Em::from_length(baseline_shift, self.size),
cluster,
safe_to_break: true,
c: '-',
@@ -402,12 +414,13 @@ fn shape_segment<'a>(
if info.glyph_id != 0 {
// Add the glyph to the shaped output.
- // TODO: Don't ignore y_advance and y_offset.
+ // TODO: Don't ignore y_advance.
ctx.glyphs.push(ShapedGlyph {
face_id,
glyph_id: info.glyph_id as u16,
x_advance: face.to_em(pos[i].x_advance),
x_offset: face.to_em(pos[i].x_offset),
+ y_offset: face.to_em(pos[i].y_offset),
cluster: base + cluster,
safe_to_break: !info.unsafe_to_break(),
c: text[cluster ..].chars().next().unwrap(),
@@ -478,6 +491,7 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, face_id: FaceI
glyph_id: 0,
x_advance,
x_offset: Em::zero(),
+ y_offset: Em::from_length(ctx.styles.get(TextNode::BASELINE), ctx.size),
cluster: base + cluster,
safe_to_break: true,
c,
@@ -602,12 +616,6 @@ fn tags(styles: StyleChain) -> Vec<Feature> {
Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
}
- match styles.get(TextNode::NUMBER_POSITION) {
- NumberPosition::Normal => {}
- NumberPosition::Subscript => feat(b"subs", 1),
- NumberPosition::Superscript => feat(b"sups", 1),
- }
-
if styles.get(TextNode::SLASHED_ZERO) {
feat(b"zero", 1);
}
diff --git a/src/library/text/shift.rs b/src/library/text/shift.rs
new file mode 100644
index 00000000..4eacd3c8
--- /dev/null
+++ b/src/library/text/shift.rs
@@ -0,0 +1,178 @@
+use super::{variant, TextNode, TextSize};
+use crate::font::FontStore;
+use crate::library::prelude::*;
+use crate::util::EcoString;
+
+/// Sub or superscript text. The text is rendered smaller and its baseline is raised.
+///
+/// To provide the best typography possible, we first try to transform the
+/// text to superscript codepoints. If that fails, we fall back to rendering
+/// shrunk normal letters in a raised way.
+#[derive(Debug, Hash)]
+pub struct ShiftNode<const S: ScriptKind>(pub Content);
+
+/// Shift the text into superscript.
+pub type SuperNode = ShiftNode<SUPERSCRIPT>;
+
+/// Shift the text into subscript.
+pub type SubNode = ShiftNode<SUBSCRIPT>;
+
+#[node]
+impl<const S: ScriptKind> ShiftNode<S> {
+ /// Whether to prefer the dedicated sub- and superscript characters of the font.
+ pub const TYPOGRAPHIC: bool = true;
+ /// The baseline shift for synthetic sub- and superscripts.
+ pub const BASELINE: RawLength =
+ Em::new(if S == SUPERSCRIPT { -0.5 } else { 0.2 }).into();
+ /// The font size for synthetic sub- and superscripts.
+ pub const SIZE: TextSize = TextSize(Em::new(0.6).into());
+
+ fn construct(_: &mut Machine, args: &mut Args) -> TypResult<Content> {
+ Ok(Content::show(Self(args.expect("body")?)))
+ }
+}
+
+impl<const S: ScriptKind> Show for ShiftNode<S> {
+ fn unguard(&self, _: Selector) -> ShowNode {
+ Self(self.0.clone()).pack()
+ }
+
+ fn encode(&self, _: StyleChain) -> Dict {
+ dict! { "body" => Value::Content(self.0.clone()) }
+ }
+
+ fn realize(&self, ctx: &mut Context, styles: StyleChain) -> TypResult<Content> {
+ let mut transformed = None;
+ if styles.get(Self::TYPOGRAPHIC) {
+ if let Some(text) = search_text(&self.0, S) {
+ if is_shapable(&mut ctx.fonts, &text, styles) {
+ transformed = Some(Content::Text(text));
+ }
+ }
+ };
+
+ Ok(transformed.unwrap_or_else(|| {
+ let mut map = StyleMap::new();
+ map.set(TextNode::BASELINE, styles.get(Self::BASELINE));
+ map.set(TextNode::SIZE, styles.get(Self::SIZE));
+ self.0.clone().styled_with_map(map)
+ }))
+ }
+}
+
+/// Find and transform the text contained in `content` iff it only consists of
+/// `Text`, `Space`, and `Empty` leaf nodes. The text is transformed to the
+/// given script kind.
+fn search_text(content: &Content, mode: ScriptKind) -> Option<EcoString> {
+ match content {
+ Content::Text(_) => {
+ if let Content::Text(t) = content {
+ if let Some(sup) = convert_script(t, mode) {
+ return Some(sup);
+ }
+ }
+ None
+ }
+ Content::Space => Some(' '.into()),
+ Content::Empty => Some(EcoString::new()),
+ Content::Sequence(seq) => {
+ let mut full = EcoString::new();
+ for item in seq.iter() {
+ match search_text(item, mode) {
+ Some(text) => full.push_str(&text),
+ None => return None,
+ }
+ }
+ Some(full)
+ }
+ _ => None,
+ }
+}
+
+/// Checks whether the first retrievable family contains all code points of the
+/// given string.
+fn is_shapable(fonts: &mut FontStore, text: &str, styles: StyleChain) -> bool {
+ for family in styles.get(TextNode::FAMILY).iter() {
+ if let Some(face_id) = fonts.select(family.as_str(), variant(styles)) {
+ let ttf = fonts.get(face_id).ttf();
+ return text.chars().all(|c| ttf.glyph_index(c).is_some());
+ }
+ }
+
+ false
+}
+
+/// Convert a string to sub- or superscript codepoints if all characters
+/// can be mapped to such a codepoint.
+fn convert_script(text: &str, mode: ScriptKind) -> Option<EcoString> {
+ let mut result = EcoString::with_capacity(text.len());
+ let converter = match mode {
+ SUPERSCRIPT => to_superscript_codepoint,
+ SUBSCRIPT | _ => to_subscript_codepoint,
+ };
+
+ for c in text.chars() {
+ match converter(c) {
+ Some(c) => result.push(c),
+ None => return None,
+ }
+ }
+
+ Some(result)
+}
+
+/// Convert a character to its corresponding Unicode superscript.
+fn to_superscript_codepoint(c: char) -> Option<char> {
+ char::from_u32(match c {
+ '0' => 0x2070,
+ '1' => 0x00B9,
+ '2' => 0x00B2,
+ '3' => 0x00B3,
+ '4' ..= '9' => 0x2070 + (c as u32 + 4 - '4' as u32),
+ '+' => 0x207A,
+ '-' => 0x207B,
+ '=' => 0x207C,
+ '(' => 0x207D,
+ ')' => 0x207E,
+ 'n' => 0x207F,
+ 'i' => 0x2071,
+ ' ' => 0x0020,
+ _ => return None,
+ })
+}
+
+/// Convert a character to its corresponding Unicode subscript.
+fn to_subscript_codepoint(c: char) -> Option<char> {
+ char::from_u32(match c {
+ '0' => 0x2080,
+ '1' ..= '9' => 0x2080 + (c as u32 - '0' as u32),
+ '+' => 0x208A,
+ '-' => 0x208B,
+ '=' => 0x208C,
+ '(' => 0x208D,
+ ')' => 0x208E,
+ 'a' => 0x2090,
+ 'e' => 0x2091,
+ 'o' => 0x2092,
+ 'x' => 0x2093,
+ 'h' => 0x2095,
+ 'k' => 0x2096,
+ 'l' => 0x2097,
+ 'm' => 0x2098,
+ 'n' => 0x2099,
+ 'p' => 0x209A,
+ 's' => 0x209B,
+ 't' => 0x209C,
+ ' ' => 0x0020,
+ _ => return None,
+ })
+}
+
+/// A category of script.
+pub type ScriptKind = usize;
+
+/// Text that is rendered smaller and raised, also known as superior.
+const SUPERSCRIPT: ScriptKind = 0;
+
+/// Text that is rendered smaller and lowered, also known as inferior.
+const SUBSCRIPT: ScriptKind = 1;