From b71113d37a29bab5c7dc4b501c33ee9afbdb8213 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 8 Mar 2022 19:49:26 +0100 Subject: [PATCH] Hyphenation --- Cargo.lock | 7 ++++ Cargo.toml | 1 + src/library/text/mod.rs | 2 + src/library/text/par.rs | 76 ++++++++++++++++++++++++++++++----- src/library/text/shaping.rs | 31 +++++++++++++- tests/ref/text/hyphenate.png | Bin 0 -> 4979 bytes tests/ref/text/justify.png | Bin 11615 -> 11709 bytes tests/typ/text/hyphenate.typ | 14 +++++++ tests/typ/text/justify.typ | 11 ++++- 9 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 tests/ref/text/hyphenate.png create mode 100644 tests/typ/text/hyphenate.typ diff --git a/Cargo.lock b/Cargo.lock index 4a6d4f0e8..b4fd7b72d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,6 +240,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "hypher" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29349e08e99b98d0e16a0ca738d181d5c73431a9a46b78918318c4bc9b10106" + [[package]] name = "iai" version = "0.1.1" @@ -806,6 +812,7 @@ dependencies = [ "either", "flate2", "fxhash", + "hypher", "iai", "image", "kurbo", diff --git a/Cargo.toml b/Cargo.toml index 3971b9714..ed265b1a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ serde = { version = "1", features = ["derive"] } typed-arena = "2" # Text and font handling +hypher = "0.1" kurbo = "0.8" ttf-parser = "0.12" rustybuzz = "0.4" diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs index 0df59007b..1ce3518ce 100644 --- a/src/library/text/mod.rs +++ b/src/library/text/mod.rs @@ -1,3 +1,5 @@ +//! Text shaping and paragraph layout. + mod deco; mod link; mod par; diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 812231c2e..70cac1be5 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -27,12 +27,17 @@ pub enum ParChild { #[class] impl ParNode { + /// An ISO 639-1 language code. + pub const LANG: Option = None; /// The direction for text and inline objects. pub const DIR: Dir = Dir::LTR; /// How to align text and inline objects in their line. pub const ALIGN: Align = Align::Left; /// Whether to justify text in its line. pub const JUSTIFY: bool = false; + /// Whether to hyphenate text to improve line breaking. When `auto`, words + /// will will be hyphenated if and only if justification is enabled. + pub const HYPHENATE: Smart = Smart::Auto; /// The spacing between lines (dependent on scaled font size). pub const LEADING: Linear = Relative::new(0.65).into(); /// The extra spacing between paragraphs (dependent on scaled font size). @@ -49,13 +54,14 @@ impl ParNode { } fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> { + let lang = args.named::>("lang")?; + let mut dir = - args.named("lang")? - .map(|iso: EcoString| match iso.to_lowercase().as_str() { - "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL, - "en" | "fr" | "de" => Dir::LTR, - _ => Dir::LTR, - }); + lang.clone().flatten().map(|iso| match iso.to_lowercase().as_str() { + "ar" | "dv" | "fa" | "he" | "ks" | "pa" | "ps" | "sd" | "ug" | "ur" + | "yi" => Dir::RTL, + _ => Dir::LTR, + }); if let Some(Spanned { v, span }) = args.named::>("dir")? { if v.axis() != SpecAxis::Horizontal { @@ -74,9 +80,11 @@ impl ParNode { dir.map(|dir| dir.start().into()) }; + styles.set_opt(Self::LANG, lang); styles.set_opt(Self::DIR, dir); styles.set_opt(Self::ALIGN, align); styles.set_opt(Self::JUSTIFY, args.named("justify")?); + styles.set_opt(Self::HYPHENATE, args.named("hyphenate")?); styles.set_opt(Self::LEADING, args.named("leading")?); styles.set_opt(Self::SPACING, args.named("spacing")?); styles.set_opt(Self::INDENT, args.named("indent")?); @@ -137,7 +145,7 @@ impl Layout for ParNode { let par = ParLayout::new(ctx, self, bidi, regions, &styles)?; // Break the paragraph into lines. - let lines = break_into_lines(&mut ctx.fonts, &par, regions.first.x); + let lines = break_into_lines(&mut ctx.fonts, &par, regions.first.x, styles); // Stack the lines into one frame per region. Ok(stack_lines(&ctx.fonts, lines, regions, styles)) @@ -278,6 +286,7 @@ impl<'a> ParLayout<'a> { fonts: &mut FontStore, mut range: Range, mandatory: bool, + hyphen: bool, ) -> LineLayout<'a> { // Find the items which bound the text range. let last_idx = self.find(range.end.saturating_sub(1)).unwrap(); @@ -308,7 +317,10 @@ impl<'a> ParLayout<'a> { // empty string. if !shifted.is_empty() || rest.is_empty() { // Reshape that part. - let reshaped = shaped.reshape(fonts, shifted); + let mut reshaped = shaped.reshape(fonts, shifted); + if hyphen { + reshaped.push_hyphen(fonts); + } last = Some(ParItem::Text(reshaped)); } @@ -524,6 +536,7 @@ fn break_into_lines<'a>( fonts: &mut FontStore, par: &'a ParLayout<'a>, width: Length, + styles: StyleChain, ) -> Vec> { // The already determined lines and the current line attempt. let mut lines = vec![]; @@ -531,9 +544,9 @@ fn break_into_lines<'a>( let mut last = None; // Find suitable line breaks. - for (end, mandatory) in LineBreakIterator::new(&par.bidi.text) { + for (end, mandatory, hyphen) in breakpoints(&par.bidi.text, styles) { // Compute the line and its size. - let mut line = par.line(fonts, start .. end, mandatory); + let mut line = par.line(fonts, start .. end, mandatory, hyphen); // If the line doesn't fit anymore, we push the last fitting attempt // into the stack and rebuild the line from its end. The resulting @@ -542,7 +555,7 @@ fn break_into_lines<'a>( if let Some((last_line, last_end)) = last.take() { lines.push(last_line); start = last_end; - line = par.line(fonts, start .. end, mandatory); + line = par.line(fonts, start .. end, mandatory, hyphen); } } @@ -565,6 +578,47 @@ fn break_into_lines<'a>( lines } +/// Determine all possible points in the text where lines can broken. +fn breakpoints<'a>( + text: &'a str, + styles: StyleChain, +) -> impl Iterator + 'a { + let mut lang = None; + if styles.get(ParNode::HYPHENATE).unwrap_or(styles.get(ParNode::JUSTIFY)) { + lang = styles + .get_ref(ParNode::LANG) + .as_ref() + .and_then(|iso| iso.as_bytes().try_into().ok()) + .and_then(hypher::Lang::from_iso); + } + + let breaks = LineBreakIterator::new(text); + let mut last = 0; + + if let Some(lang) = lang { + Either::Left(breaks.flat_map(move |(end, mandatory)| { + let word = &text[last .. end]; + let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic()); + let suffix = last + trimmed.len(); + let mut start = std::mem::replace(&mut last, end); + if trimmed.is_empty() { + Either::Left([(end, mandatory, false)].into_iter()) + } else { + Either::Right(hypher::hyphenate(trimmed, lang).map(move |syllable| { + start += syllable.len(); + if start == suffix { + start = end; + } + let hyphen = start < end; + (start, mandatory && !hyphen, hyphen) + })) + } + })) + } else { + Either::Right(breaks.map(|(e, m)| (e, m, false))) + } +} + /// Combine the lines into one frame per region. fn stack_lines( fonts: &FontStore, diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs index 26c8daf35..b467abf70 100644 --- a/src/library/text/shaping.rs +++ b/src/library/text/shaping.rs @@ -135,6 +135,34 @@ impl<'a> ShapedText<'a> { } } + /// Push a hyphen to end of the text. + pub fn push_hyphen(&mut self, fonts: &mut FontStore) { + // When there are no glyphs, we just use the vertical metrics of the + // first available font. + let size = self.styles.get(TextNode::SIZE).abs; + let variant = variant(self.styles); + families(self.styles).find_map(|family| { + // Allow hyphens to overhang a bit. + const INSET: f64 = 0.4; + let face_id = fonts.select(family, variant)?; + let face = fonts.get(face_id); + let ttf = face.ttf(); + let glyph_id = ttf.glyph_index('-')?; + let x_advance = face.to_em(ttf.glyph_hor_advance(glyph_id)?); + self.size.x += INSET * x_advance.resolve(size); + self.glyphs.to_mut().push(ShapedGlyph { + face_id, + glyph_id: glyph_id.0, + x_advance, + x_offset: Em::zero(), + text_index: self.text.len(), + safe_to_break: true, + is_space: false, + }); + Some(()) + }); + } + /// Find the subslice of glyphs that represent the given text range if both /// sides are safe to break. fn slice_safe_to_break(&self, text_range: Range) -> Option<&[ShapedGlyph]> { @@ -531,8 +559,9 @@ fn measure( if glyphs.is_empty() { // When there are no glyphs, we just use the vertical metrics of the // first available font. + let variant = variant(styles); for family in families(styles) { - if let Some(face_id) = fonts.select(family, variant(styles)) { + if let Some(face_id) = fonts.select(family, variant) { expand(fonts.get(face_id)); break; } diff --git a/tests/ref/text/hyphenate.png b/tests/ref/text/hyphenate.png new file mode 100644 index 0000000000000000000000000000000000000000..050cab12bc1848d06d625f6bf70327a31c001c4d GIT binary patch literal 4979 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 At^fc4 literal 11615 zcmaKyWl&q~yY3T6Aw@%Jai>6WiaQi{N^y60iff?3O7Rw#LJP&ExF%R}cXx^t+~M%P z|9xhkIWv3zX02Ik?oZE$=f38aEAqXHEH(xS1^@uSmY0+I0000%Pj3MDDFEnJkzWP? zSn1`Z-f4Kx9V}XFywoN|9Qz%zQB};0yy^-uqhli2e^x1-YxXWWm#8Z&RH_o5J8DxQ zO((-bq^RLg*(vvhl^&0W2-R)`HY2TuU#ZCcc-g@Q?kOWeycWDfwH_YgvJ4t%;W`}{ zXpwj$&8VRE`NP-Tmw8XWJ}k|q((m3wUV@&z`=35WbP^2Z1F`9I_qrVVZmol+l8IYh z@^3|tx}9thg(}Y2f4$R<-J+1Y8W3=+E`zpx_H|3$7mn(ESVcG5v>!VQPd8svr@v$y zxzZtN+F8Fcl-DXYxNk7vlMY*J4{KRKO^7)E{?U<`eD&Go<>hc#oZI5c@8QTJER}hd zhra8{Z|;(9$8}h2zdBi<_45dkx0ov!q2T}stG@^3Zz>{2>u4$me~MM-Z@E>RVoL;7 zSlzEMB=jndLKFe4ikwF($B$vNZcI4be$0hPL_D_T`R1S7^@+NUjGmyZ z7~S;^OMt0st@N){*DxarZB{iDJbOk#-gcai$>^O<^=3a@nHU!-)?dRQLnCz{?&KRc zz>Gn)lzQp49rW7HgRUPK8f?jDK2jTI?4N)`z4Q4jDmHeR2?ybm z*cSQo%lps{)!+6k@>NlUC=w!2wUaU&YE2QIIwX6S7lO@|$Ry>Oc82z^(Qgtgj`arK zRfgkd?lvj&A#OC>9=)SDXRPtpK6U(n7G`>2+uTqRR_tqfcB^&ARYIGVaR=ZAK}Z29Hel z#^(_(^Fnl>;Kv@oYobPi;_L)wsoKdBs}P?&b2yaG@sSl4 zJXBln?N+Jptkg}9RngOI%&WpF@qeHMhP%4W@Cl*yF+!-|A*5Y%KRevsGEiLzz+hTd ziMU}ty`LJWGSswn8-nycR;G8W5CkrhAN>-=IZi4AE{nuMoSj>)ZY{B+?yxQuUS-3B z4uooofgYV{ceD#5N>c#^^TZ~ZTUb~2hX_q;M$7vOveli;D7#ufRE@+8OGr<`51{BY zOK4LxcUq6bC<9Mh9RGkpA7}R8DURQju(%cpZsQJhRW8PUXS5WjQXk4CX7q*8D^z29 z9MntSbj;B!5F#(1X~!l1qv7{L9sPo9nBU(c;#86VR-SUq&ryx>S$otk#@MX1a(=

x+&8o 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 Vhp`jn8vo;*mX}tMDw8k?`9C-a?x6qx diff --git a/tests/typ/text/hyphenate.typ b/tests/typ/text/hyphenate.typ new file mode 100644 index 000000000..d6f44477b --- /dev/null +++ b/tests/typ/text/hyphenate.typ @@ -0,0 +1,14 @@ +// Test hyphenation. + +--- +#set page(width: 70pt) +#set par(lang: "en", hyphenate: true) +Warm welcomes to Typst. + +#h(6pt) networks, the rest. + +--- +#set page(width: 60pt) +#set par(lang: "el", hyphenate: true) +διαμερίσματα. \ +λατρευτός diff --git a/tests/typ/text/justify.typ b/tests/typ/text/justify.typ index 7b8a28299..eb8feb61b 100644 --- a/tests/typ/text/justify.typ +++ b/tests/typ/text/justify.typ @@ -1,9 +1,16 @@ --- -#set par(indent: 14pt, spacing: 0pt, leading: 5pt, justify: true) +#set page(width: 180pt) +#set par( + lang: "en", + justify: true, + indent: 14pt, + spacing: 0pt, + leading: 5pt, +) This text is justified, meaning that spaces are stretched so that the text -forms as "block" with flush edges at both sides. +forms a "block" with flush edges at both sides. First line indents and hyphenation play nicely with justified text.