diff --git a/COMPILING.md b/COMPILING.md index fe2a9ce114e0a65554c734369352f0493eb9068f..44ce72f551a30d1124f46dace2574e3366887369 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -43,7 +43,7 @@ minimum version is listed here; you may use a newer version instead. * GObject-Introspection 0.10.8 * Gtk-doc 1.13 * Libxml2 2.9.0 -* Pango 1.44.0 +* Pango 1.46.0 The following sections describe how to install these dependencies on several systems. diff --git a/Cargo.toml b/Cargo.toml index 4560ec682d180c4f1cb1a62a7aaddaf68fdd2361..685468b17cd45f4fd95a7a8bf287fc451ca59877 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,11 +12,11 @@ cairo-svg = { version = "1.16", optional = true } gdk-pixbuf = { name = "gdk-pixbuf-2.0", version = "2.20" } gio = { name = "gio-2.0", version = "2.24" } glib = { name = "glib-2.0", version = "2.50" } -pangocairo = "1.44" +pangocairo = "1.46" [package.metadata.system-deps.'cfg(windows)'] fontconfig = { version = "1.7", optional = true } -pangoft2 = { version = "1.44", optional = true } +pangoft2 = { version = "1.46", optional = true } harfbuzz = { version = "2.0", optional = true } freetype2 = { version = "20.0.14", optional = true } cairo = { version = "1.16", optional = true } @@ -26,7 +26,7 @@ libxml2 = { name = "libxml-2.0", version = "2.9", optional = true } [package.metadata.system-deps.'cfg(not(windows))'] fontconfig = { version = "1.7" } -pangoft2 = { version = "1.44" } +pangoft2 = { version = "1.46" } cairo = "1.16" cairo-gobject = "1.16" cairo-png = "1.16" @@ -60,7 +60,7 @@ markup5ever = "0.10" nalgebra = "0.27.1" num-traits = "0.2" once_cell = "1.2.0" -pango = { version="0.14.0", features = ["v1_44"] } +pango = { version="0.14.0", features = ["v1_46"] } pangocairo = "0.14.0" rayon = "1" rctree = "0.3.3" diff --git a/configure.ac b/configure.ac index f7230c5c1ffda2073c8e9174add48912c3af4c00..85e60cc05ad0f542cbe3aae3201211121e78c13d 100644 --- a/configure.ac +++ b/configure.ac @@ -66,7 +66,7 @@ GIO_REQUIRED=2.24.0 GLIB_REQUIRED=2.50.0 HARFBUZZ_REQUIRED=2.0.0 LIBXML_REQUIRED=2.9.0 -PANGO_REQUIRED=1.44.0 +PANGO_REQUIRED=1.46.0 dnl =========================================================================== diff --git a/src/layout.rs b/src/layout.rs index 19ebd63b38feddaf1eafd8914b81f3d99985c781..c259e91079f5ad6b82c752d4d83d89a978198095 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -18,7 +18,7 @@ use crate::properties::{ ClipRule, ComputedValues, Direction, FillRule, Filter, FontFamily, FontStretch, FontStyle, FontVariant, FontWeight, MixBlendMode, Opacity, Overflow, PaintOrder, ShapeRendering, StrokeDasharray, StrokeLinecap, StrokeLinejoin, StrokeMiterlimit, TextDecoration, - TextRendering, UnicodeBidi, WritingMode, XmlLang, + TextRendering, UnicodeBidi, XmlLang, }; use crate::rect::Rect; use crate::surface_utils::shared_surface::SharedImageSurface; @@ -121,7 +121,6 @@ pub struct Text { /// Font-related properties extracted from `ComputedValues`. pub struct FontProperties { pub xml_lang: XmlLang, - pub writing_mode: WritingMode, pub unicode_bidi: UnicodeBidi, pub direction: Direction, pub font_family: FontFamily, @@ -268,14 +267,9 @@ impl FontProperties { /// /// The `writing-mode` property is passed separately, as it must come from the `` element, /// not the `` whose computed values are being passed. - pub fn new( - values: &ComputedValues, - writing_mode: WritingMode, - params: &NormalizeParams, - ) -> FontProperties { + pub fn new(values: &ComputedValues, params: &NormalizeParams) -> FontProperties { FontProperties { xml_lang: values.xml_lang(), - writing_mode, unicode_bidi: values.unicode_bidi(), direction: values.direction(), font_family: values.font_family(), diff --git a/src/property_defs.rs b/src/property_defs.rs index e9cd1352a82c5f500d1545a3065e659c00583cc2..76f9924c4ee6b2f56baf61b3f0de7522dcb5aa88 100644 --- a/src/property_defs.rs +++ b/src/property_defs.rs @@ -1022,7 +1022,10 @@ make_property!( identifiers: "normal" => Normal, "embed" => Embed, - "bidi-override" => Override, + "isolate" => Isolate, + "bidi-override" => BidiOverride, + "isolate-override" => IsolateOverride, + "plaintext" => Plaintext, ); make_property!( diff --git a/src/text.rs b/src/text.rs index 1c0c11a7ec06886f049c1ebd448e5418cbd90e32..a865c73814792fc48c5df65f989477f8cac7da0a 100644 --- a/src/text.rs +++ b/src/text.rs @@ -23,6 +23,12 @@ use crate::space::{xml_space_normalize, NormalizeDefault, XmlSpaceNormalize}; use crate::transform::Transform; use crate::xml::Attributes; +/// The state of a text layout operation. +struct LayoutContext { + /// `writing-mode` property from the `` element. + writing_mode: WritingMode, +} + /// An absolutely-positioned array of `Span`s /// /// SVG defines a "[text chunk]" to occur when a text-related element @@ -119,14 +125,14 @@ impl Chunk { impl MeasuredChunk { fn from_chunk( + layout_context: &LayoutContext, chunk: &Chunk, - text_writing_mode: WritingMode, draw_ctx: &DrawingCtx, ) -> MeasuredChunk { let mut measured_spans: Vec = chunk .spans .iter() - .map(|span| MeasuredSpan::from_span(span, text_writing_mode, draw_ctx)) + .map(|span| MeasuredSpan::from_span(layout_context, span, draw_ctx)) .collect(); // The first span contains the (dx, dy) that will be applied to the whole chunk. @@ -156,9 +162,9 @@ impl MeasuredChunk { impl PositionedChunk { fn from_measured( + layout_context: &LayoutContext, measured: &MeasuredChunk, view_params: &ViewParams, - text_writing_mode: WritingMode, chunk_x: f64, chunk_y: f64, ) -> PositionedChunk { @@ -197,7 +203,7 @@ impl PositionedChunk { Direction::Rtl => (-advance.0, advance.1), }; - let rendered_position = if text_writing_mode.is_horizontal() { + let rendered_position = if layout_context.writing_mode.is_horizontal() { (start_pos.0 + dx, start_pos.1 - baseline_offset + dy) } else { (start_pos.0 + baseline_offset + dx, start_pos.1 + dy) @@ -231,7 +237,7 @@ impl PositionedChunk { let anchor_offset = text_anchor_offset( measured.values.text_anchor(), chunk_direction, - text_writing_mode, + layout_context.writing_mode, chunk_bounds.unwrap_or_default(), ); @@ -275,14 +281,14 @@ fn compute_baseline_offset( fn text_anchor_offset( anchor: TextAnchor, direction: Direction, - text_writing_mode: WritingMode, + writing_mode: WritingMode, chunk_bounds: Rect, ) -> (f64, f64) { let (w, h) = (chunk_bounds.width(), chunk_bounds.height()); let x0 = chunk_bounds.x0; - if text_writing_mode.is_horizontal() { + if writing_mode.is_horizontal() { match (anchor, direction) { (TextAnchor::Start, Direction::Ltr) => (-x0, 0.0), (TextAnchor::Start, Direction::Rtl) => (-x0 - w, 0.0), @@ -325,8 +331,8 @@ impl Span { impl MeasuredSpan { fn from_span( + layout_context: &LayoutContext, span: &Span, - text_writing_mode: WritingMode, draw_ctx: &DrawingCtx, ) -> MeasuredSpan { let values = span.values.clone(); @@ -334,8 +340,20 @@ impl MeasuredSpan { let view_params = draw_ctx.get_view_params(); let params = NormalizeParams::new(&values, &view_params); - let properties = FontProperties::new(&values, text_writing_mode, ¶ms); - let layout = create_pango_layout(draw_ctx, &properties, &span.text); + let properties = FontProperties::new(&values, ¶ms); + + let bidi_control = BidiControl::from_unicode_bidi_and_direction( + properties.unicode_bidi, + properties.direction, + ); + + let with_control_chars = wrap_with_direction_control_chars(&span.text, &bidi_control); + let layout = create_pango_layout( + draw_ctx, + layout_context.writing_mode, + &properties, + &with_control_chars, + ); let (w, h) = layout.size(); let w = f64::from(w) / f64::from(pango::SCALE); @@ -345,7 +363,7 @@ impl MeasuredSpan { assert!(w >= 0.0); assert!(h >= 0.0); - let advance = if text_writing_mode.is_horizontal() { + let advance = if layout_context.writing_mode.is_horizontal() { (w, 0.0) } else { (0.0, w) @@ -417,7 +435,7 @@ impl PositionedSpan { fn layout( &self, acquired_nodes: &mut AcquiredNodes<'_>, - draw_ctx: &mut DrawingCtx, + draw_ctx: &DrawingCtx, view_params: &ViewParams, ) -> LayoutSpan { let params = NormalizeParams::new(&self.values, view_params); @@ -477,7 +495,7 @@ fn children_to_chunks( node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, - draw_ctx: &mut DrawingCtx, + draw_ctx: &DrawingCtx, dx: f64, dy: f64, depth: usize, @@ -518,8 +536,14 @@ fn children_to_chunks( } Element::Link(ref link) => { - // TSpan::default tes all offsets to 0, + // TSpan::default sets all offsets to 0, // which is what we want in links. + // + // FIXME: This is the only place in the code where an element's method (TSpan::to_chunks) + // is called with a node that is not the element itself: here, `child` is a Link, not a TSpan. + // + // The code works because the `tspan` is dropped immediately after calling to_chunks and no + // references are retained for it. let tspan = TSpan::default(); let cascaded = CascadedValues::new(cascaded, &child); tspan.to_chunks( @@ -670,7 +694,7 @@ impl Text { node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, - draw_ctx: &mut DrawingCtx, + draw_ctx: &DrawingCtx, x: f64, y: f64, ) -> Vec { @@ -733,8 +757,6 @@ impl Draw for Text { let stacking_ctx = StackingContext::new(acquired_nodes, &elt, values.transform(), values); - let text_writing_mode = values.writing_mode(); - draw_ctx.with_discrete_layer( &stacking_ctx, acquired_nodes, @@ -742,6 +764,10 @@ impl Draw for Text { clipping, None, &mut |an, dc| { + let layout_context = LayoutContext { + writing_mode: values.writing_mode(), + }; + let mut x = self.x.to_user(¶ms); let mut y = self.y.to_user(¶ms); @@ -749,7 +775,7 @@ impl Draw for Text { let mut measured_chunks = Vec::new(); for chunk in &chunks { - measured_chunks.push(MeasuredChunk::from_chunk(chunk, text_writing_mode, dc)); + measured_chunks.push(MeasuredChunk::from_chunk(&layout_context, chunk, dc)); } let mut positioned_chunks = Vec::new(); @@ -758,9 +784,9 @@ impl Draw for Text { let chunk_y = chunk.y.unwrap_or(y); let positioned = PositionedChunk::from_measured( + &layout_context, chunk, &view_params, - text_writing_mode, chunk_x, chunk_y, ); @@ -910,7 +936,7 @@ impl TSpan { node: &Node, acquired_nodes: &mut AcquiredNodes<'_>, cascaded: &CascadedValues<'_>, - draw_ctx: &mut DrawingCtx, + draw_ctx: &DrawingCtx, chunks: &mut Vec, dx: f64, dy: f64, @@ -1023,15 +1049,6 @@ impl From for pango::Direction { } } -impl From for pango::Alignment { - fn from(d: Direction) -> pango::Alignment { - match d { - Direction::Ltr => pango::Alignment::Left, - Direction::Rtl => pango::Alignment::Right, - } - } -} - impl From for pango::Direction { fn from(m: WritingMode) -> pango::Direction { use WritingMode::*; @@ -1053,17 +1070,131 @@ impl From for pango::Gravity { } } -fn create_pango_layout(draw_ctx: &DrawingCtx, props: &FontProperties, text: &str) -> pango::Layout { +/// Constants with Unicode's directional formatting characters +/// +/// https://unicode.org/reports/tr9/#Directional_Formatting_Characters +mod directional_formatting_characters { + /// Left-to-Right Embedding + /// + /// Treat the following text as embedded left-to-right. + pub const LRE: char = '\u{202a}'; + + /// Right-to-Left Embedding + /// + /// Treat the following text as embedded right-to-left. + pub const RLE: char = '\u{202b}'; + + /// Left-to-Right Override + /// + /// Force following characters to be treated as strong left-to-right characters. + pub const LRO: char = '\u{202d}'; + + /// Right-to-Left Override + /// + /// Force following characters to be treated as strong right-to-left characters. + pub const RLO: char = '\u{202e}'; + + /// Pop Directional Formatting + /// + /// End the scope of the last LRE, RLE, RLO, or LRO. + pub const PDF: char = '\u{202c}'; + + /// Left-to-Right Isolate + /// + /// Treat the following text as isolated and left-to-right. + pub const LRI: char = '\u{2066}'; + + /// Right-to-Left Isolate + /// + /// Treat the following text as isolated and right-to-left. + pub const RLI: char = '\u{2067}'; + + /// First Strong Isolate + /// + /// Treat the following text as isolated and in the direction of its first strong + /// directional character that is not inside a nested isolate. + pub const FSI: char = '\u{2068}'; + + /// Pop Directional Isolate + /// + /// End the scope of the last LRI, RLI, or FSI. + pub const PDI: char = '\u{2069}'; +} + +/// Unicode control characters to be inserted when `unicode-bidi` is specified. +/// +/// The `unicode-bidi` property is used to change the embedding of a text span within +/// another. This struct contains slices with the control characters that must be +/// inserted into the text stream at the span's limits so that the bidi/shaping engine +/// will know what to do. +struct BidiControl { + start: &'static [char], + end: &'static [char], +} + +impl BidiControl { + /// Creates a `BidiControl` from the properties that determine it. + /// + /// See the table titled "Bidi control codes injected..." in + /// https://www.w3.org/TR/css-writing-modes-3/#unicode-bidi + #[rustfmt::skip] + fn from_unicode_bidi_and_direction(unicode_bidi: UnicodeBidi, direction: Direction) -> BidiControl { + use UnicodeBidi::*; + use Direction::*; + use directional_formatting_characters::*; + + let (start, end) = match (unicode_bidi, direction) { + (Normal, _) => (&[][..], &[][..]), + (Embed, Ltr) => (&[LRE][..], &[PDF][..]), + (Embed, Rtl) => (&[RLE][..], &[PDF][..]), + (Isolate, Ltr) => (&[LRI][..], &[PDI][..]), + (Isolate, Rtl) => (&[RLI][..], &[PDI][..]), + (BidiOverride, Ltr) => (&[LRO][..], &[PDF][..]), + (BidiOverride, Rtl) => (&[RLO][..], &[PDF][..]), + (IsolateOverride, Ltr) => (&[FSI, LRO][..], &[PDF, PDI][..]), + (IsolateOverride, Rtl) => (&[FSI, RLO][..], &[PDF, PDI][..]), + (Plaintext, Ltr) => (&[FSI][..], &[PDI][..]), + (Plaintext, Rtl) => (&[FSI][..], &[PDI][..]), + }; + + BidiControl { start, end } + } +} + +/// Prepends and appends Unicode directional formatting characters. +fn wrap_with_direction_control_chars(s: &str, bidi_control: &BidiControl) -> String { + let mut res = + String::with_capacity(s.len() + bidi_control.start.len() + bidi_control.end.len()); + + for &ch in bidi_control.start { + res.push(ch); + } + + res.push_str(s); + + for &ch in bidi_control.end { + res.push(ch); + } + + res +} + +fn create_pango_layout( + draw_ctx: &DrawingCtx, + writing_mode: WritingMode, + props: &FontProperties, + text: &str, +) -> pango::Layout { let pango_context = draw_ctx.create_pango_context(); if let XmlLang(Some(ref lang)) = props.xml_lang { pango_context.set_language(&pango::Language::from_string(lang.as_str())); } - pango_context.set_base_gravity(pango::Gravity::from(props.writing_mode)); + pango_context.set_base_gravity(pango::Gravity::from(writing_mode)); match (props.unicode_bidi, props.direction) { - (UnicodeBidi::Override, _) | (UnicodeBidi::Embed, _) => { + (UnicodeBidi::BidiOverride, _) | (UnicodeBidi::Embed, _) => { pango_context.set_base_dir(pango::Direction::from(props.direction)); } @@ -1072,11 +1203,35 @@ fn create_pango_layout(draw_ctx: &DrawingCtx, props: &FontProperties, text: &str } (_, _) => { - pango_context.set_base_dir(pango::Direction::from(props.writing_mode)); + pango_context.set_base_dir(pango::Direction::from(writing_mode)); } } - let mut font_desc = pango_context.font_description().unwrap(); + let layout = pango::Layout::new(&pango_context); + + let attr_list = pango::AttrList::new(); + add_pango_attributes(&attr_list, props, 0, text.len()); + + layout.set_attributes(Some(&attr_list)); + layout.set_text(text); + layout.set_auto_dir(false); + + layout +} + +/// Adds Pango attributes, suitable for a span of text, to an `AttrList`. +fn add_pango_attributes( + attr_list: &pango::AttrList, + props: &FontProperties, + start_index: usize, + end_index: usize, +) { + let start_index: u32 = cast::u32(start_index).expect("Pango attribute index must fit in u32"); + let end_index: u32 = cast::u32(end_index).expect("Pango attribute index must fit in u32"); + + let mut attributes = Vec::new(); + + let mut font_desc = pango::FontDescription::new(); font_desc.set_family(props.font_family.as_str()); font_desc.set_style(pango::Style::from(props.font_style)); @@ -1089,48 +1244,43 @@ fn create_pango_layout(draw_ctx: &DrawingCtx, props: &FontProperties, text: &str font_desc.set_size(to_pango_units(props.font_size)); - let layout = pango::Layout::new(&pango_context); - layout.set_auto_dir(false); - layout.set_font_description(Some(&font_desc)); - - // FIXME: For now we ignore the `line-height` property, even though we parse it. - // We would need to do something like this: - // - // layout.set_line_spacing(0.0); // "actually use the spacing I'll give you" - // layout.set_spacing(to_pango_units(???)); - // - // However, Layout::set_spacing() takes an inter-line spacing (from the baseline of - // one line to the top of the next line), not the line height (from baseline to - // baseline). - // - // Maybe we need to implement layout of individual lines by hand. - - let attr_list = pango::AttrList::new(); + attributes.push(pango::Attribute::new_font_desc(&font_desc)); - attr_list.insert(pango::Attribute::new_letter_spacing(to_pango_units( + attributes.push(pango::Attribute::new_letter_spacing(to_pango_units( props.letter_spacing, ))); + if props.text_decoration.overline { + attributes.push(pango::Attribute::new_overline(pango::Overline::Single)); + } + if props.text_decoration.underline { - attr_list.insert(pango::Attribute::new_underline(pango::Underline::Single)); + attributes.push(pango::Attribute::new_underline(pango::Underline::Single)); } if props.text_decoration.strike { - attr_list.insert(pango::Attribute::new_strikethrough(true)); + attributes.push(pango::Attribute::new_strikethrough(true)); } // FIXME: Using the "smcp" OpenType feature only works for fonts that support it. We // should query if the font supports small caps, and synthesize them if it doesn't. if props.font_variant == FontVariant::SmallCaps { // smcp - small capitals - https://docs.microsoft.com/en-ca/typography/opentype/spec/features_pt#smcp - attr_list.insert(pango::Attribute::new_font_features("'smcp' 1")); + attributes.push(pango::Attribute::new_font_features("'smcp' 1")); } - layout.set_attributes(Some(&attr_list)); - layout.set_alignment(pango::Alignment::from(props.direction)); - layout.set_text(text); + // Set the range in each attribute - layout + for attr in &mut attributes { + attr.set_start_index(start_index); + attr.set_end_index(end_index); + } + + // Add the attributes to the attr_list + + for attr in attributes { + attr_list.insert(attr); + } } #[cfg(test)] diff --git a/tests/fixtures/text/unicode-bidi-override-ref.svg b/tests/fixtures/text/unicode-bidi-override-ref.svg new file mode 100644 index 0000000000000000000000000000000000000000..2357471bb569eef918a969cff4ad7f569b9d34a4 --- /dev/null +++ b/tests/fixtures/text/unicode-bidi-override-ref.svg @@ -0,0 +1,6 @@ + + + + + ÉAppAÉÉAp + diff --git a/tests/fixtures/text/unicode-bidi-override.svg b/tests/fixtures/text/unicode-bidi-override.svg new file mode 100644 index 0000000000000000000000000000000000000000..5b230d2611099a694c10e6d65923468100a6e060 --- /dev/null +++ b/tests/fixtures/text/unicode-bidi-override.svg @@ -0,0 +1,6 @@ + + + + + ÉApÉApÉAp + diff --git a/tests/src/text.rs b/tests/src/text.rs index cc3c1527eb8117eaac2ffab67515f35f6dd02369..779fb12747d228b4170d051f041ce56dd60d2ea6 100644 --- a/tests/src/text.rs +++ b/tests/src/text.rs @@ -39,3 +39,9 @@ test_svg_reference!( "tests/fixtures/text/span-bounds-when-offset-by-dx.svg", "tests/fixtures/text/span-bounds-when-offset-by-dx-ref.svg" ); + +test_svg_reference!( + unicode_bidi_override, + "tests/fixtures/text/unicode-bidi-override.svg", + "tests/fixtures/text/unicode-bidi-override-ref.svg" +);