From 8a57395ee48ecee02c2eb833d232979730f0e445 Mon Sep 17 00:00:00 2001 From: damaxwell Date: Wed, 19 Jul 2023 02:25:24 -0800 Subject: [PATCH] Support OpenType writing script (#1697) --- crates/typst-library/src/layout/par.rs | 19 +++++++---- crates/typst-library/src/text/mod.rs | 25 +++++++++++++++ crates/typst-library/src/text/shaping.rs | 5 +++ crates/typst/src/doc.rs | 39 +++++++++++++++++++++++ tests/ref/text/lang.png | Bin 4207 -> 5252 bytes tests/ref/text/shaping.png | Bin 2606 -> 3902 bytes tests/typ/text/lang.typ | 20 ++++++++++++ tests/typ/text/shaping.typ | 10 ++++++ 8 files changed, 112 insertions(+), 6 deletions(-) diff --git a/crates/typst-library/src/layout/par.rs b/crates/typst-library/src/layout/par.rs index fe86f62a5..6d1b653f7 100644 --- a/crates/typst-library/src/layout/par.rs +++ b/crates/typst-library/src/layout/par.rs @@ -750,6 +750,7 @@ fn shape_range<'a>( spans: &SpanMapper, styles: StyleChain<'a>, ) { + let script = TextElem::script_in(styles); let lang = TextElem::lang_in(styles); let region = TextElem::region_in(styles); let mut process = |range: Range, level: BidiLevel| { @@ -763,25 +764,31 @@ fn shape_range<'a>( let mut prev_script = Script::Unknown; let mut cursor = range.start; - // Group by embedding level and script. + // Group by embedding level and script. If the text's script is explicitly + // set (rather than inferred from the glpyhs), we keep the script at an + // unchanging `Script::Unknown` so that only level changes cause breaks. for i in range.clone() { if !bidi.text.is_char_boundary(i) { continue; } let level = bidi.levels[i]; - let script = - bidi.text[i..].chars().next().map_or(Script::Unknown, |c| c.script()); + let curr_script = match script { + Smart::Auto => { + bidi.text[i..].chars().next().map_or(Script::Unknown, |c| c.script()) + } + Smart::Custom(_) => Script::Unknown, + }; - if level != prev_level || !is_compatible(script, prev_script) { + if level != prev_level || !is_compatible(curr_script, prev_script) { if cursor < i { process(cursor..i, prev_level); } cursor = i; prev_level = level; - prev_script = script; + prev_script = curr_script; } else if is_generic_script(prev_script) { - prev_script = script; + prev_script = curr_script; } } diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index f7c15c29d..6cfcb7c7f 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -265,6 +265,31 @@ pub struct TextElem { #[default(BottomEdge::Metric(BottomEdgeMetric::Baseline))] pub bottom_edge: BottomEdge, + /// The OpenType writing script setting. + /// + /// The combination of `{script}` and `{lang}` determine how + /// font features, such as glyph substitution, are implemented. + /// Frequently the value is a modified (all-lowercase) ISO 15924 script identifier, and + /// the `math` writing script is used for features appropriate + /// for mathematical symbols. + /// + /// When set to `{auto}`, the default and recommended setting, + /// an appropriate script is chosen for each block of characters + /// sharing a common Unicode script property. + /// + /// ```example + /// #let scedilla = [Ş] + /// #set text(font: "Linux Libertine", size: 20pt) + /// #scedilla // S with a cedilla + /// + /// #set text(script: "latn", lang: "ro") + /// #scedilla // S with a subscript comma + /// + /// #set text(script: "grek", lang: "ro") + /// #scedilla // S with a cedilla + /// ``` + pub script: Smart, + /// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639) /// /// Setting the correct language affects various parts of Typst: diff --git a/crates/typst-library/src/text/shaping.rs b/crates/typst-library/src/text/shaping.rs index 3ccac635a..53289e263 100644 --- a/crates/typst-library/src/text/shaping.rs +++ b/crates/typst-library/src/text/shaping.rs @@ -634,6 +634,11 @@ fn shape_segment( let mut buffer = UnicodeBuffer::new(); buffer.push_str(text); buffer.set_language(language(ctx.styles)); + if let Some(script) = TextElem::script_in(ctx.styles).as_custom().and_then(|script| { + rustybuzz::Script::from_iso15924_tag(Tag::from_bytes(script.as_bytes())) + }) { + buffer.set_script(script) + } buffer.set_direction(match ctx.dir { Dir::LTR => rustybuzz::Direction::LeftToRight, Dir::RTL => rustybuzz::Direction::RightToLeft, diff --git a/crates/typst/src/doc.rs b/crates/typst/src/doc.rs index 8532934cc..3eaf4c599 100644 --- a/crates/typst/src/doc.rs +++ b/crates/typst/src/doc.rs @@ -514,6 +514,45 @@ impl Glyph { } } +/// An ISO 15924-type script identifier +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct WritingScript([u8; 4], u8); + +impl WritingScript { + /// Return the script as an all lowercase string slice. + pub fn as_str(&self) -> &str { + std::str::from_utf8(&self.0[..usize::from(self.1)]).unwrap_or_default() + } + + /// Return the description of the script as raw bytes. + pub fn as_bytes(&self) -> &[u8; 4] { + &self.0 + } +} + +impl FromStr for WritingScript { + type Err = &'static str; + + /// Construct a region from its ISO 15924 code. + fn from_str(iso: &str) -> Result { + let len = iso.len(); + if matches!(len, 3..=4) && iso.is_ascii() { + let mut bytes = [b' '; 4]; + bytes[..len].copy_from_slice(iso.as_bytes()); + bytes.make_ascii_lowercase(); + Ok(Self(bytes, len as u8)) + } else { + Err("expected three or four letter script code (ISO 15924 or 'math')") + } + } +} + +cast! { + WritingScript, + self => self.as_str().into_value(), + string: EcoString => Self::from_str(&string)?, +} + /// An identifier for a natural language. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Lang([u8; 3], u8); diff --git a/tests/ref/text/lang.png b/tests/ref/text/lang.png index 9ec8815294b0a5946592d68d62c72a2e7c34fc9a..a5ae89796aadf3a9ab72aef0fadc38a3920e872d 100644 GIT binary patch literal 5252 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 F_CG#BjYa?f literal 4207 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 literal 2606 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 diff --git a/tests/typ/text/lang.typ b/tests/typ/text/lang.typ index a70b4d633..7f1ae1fc3 100644 --- a/tests/typ/text/lang.typ +++ b/tests/typ/text/lang.typ @@ -22,6 +22,26 @@ #text(lang: "uk")[Бб] #text(lang: "sr")[Бб] +--- +// Verify that writing script/language combination has an effect +#{ + set text(size:20pt) + set text(script: "latn", lang: "en") + [Ş ] + set text(script: "latn", lang: "ro") + [Ş ] + set text(script: "grek", lang: "ro") + [Ş ] +} + +--- +// Error: 19-23 expected string or auto, found none +#set text(script: none) + +--- +// Error: 19-23 expected three or four letter script code (ISO 15924 or 'math') +#set text(script: "ab") + --- // Error: 17-21 expected string, found none #set text(lang: none) diff --git a/tests/typ/text/shaping.typ b/tests/typ/text/shaping.typ index 3a99815bb..3a8d54110 100644 --- a/tests/typ/text/shaping.typ +++ b/tests/typ/text/shaping.typ @@ -11,6 +11,16 @@ ABCअपार्टमेंट // if we didn't separate by script. अ पा र् ट में ट +--- +// A forced `latn` script inhibits Devanagari font features. +#set text(script: "latn") +ABCअपार्टमेंट + +--- +// A forced `deva` script enables Devanagari font features. +#set text(script: "deva") +ABCअपार्टमेंट + --- // Test that RTL safe-to-break doesn't panic even though newline // doesn't exist in shaping output.