use markup5ever::{expanded_name, local_name, namespace_url, ns, QualName};
use pango::IsAttribute;
use std::cell::RefCell;
use std::convert::TryFrom;
use std::rc::Rc;
use crate::bbox::BoundingBox;
use crate::document::{AcquiredNodes, NodeId};
use crate::drawing_ctx::{create_pango_context, DrawingCtx, FontOptions, Viewport};
use crate::element::{set_attribute, ElementData, ElementTrait};
use crate::error::*;
use crate::layout::{self, FontProperties, Layer, LayerKind, StackingContext, Stroke, TextSpan};
use crate::length::*;
use crate::node::{CascadedValues, Node, NodeBorrow};
use crate::paint_server::PaintSource;
use crate::parsers::{CommaSeparatedList, Parse, ParseValue};
use crate::properties::{
ComputedValues, Direction, FontStretch, FontStyle, FontVariant, FontWeight, PaintOrder,
TextAnchor, TextRendering, UnicodeBidi, WritingMode, XmlLang, XmlSpace,
};
use crate::rect::Rect;
use crate::rsvg_log;
use crate::session::Session;
use crate::space::{xml_space_normalize, NormalizeDefault, XmlSpaceNormalize};
use crate::transform::{Transform, ValidTransform};
use crate::xml::Attributes;
struct LayoutContext {
writing_mode: WritingMode,
transform: ValidTransform,
font_options: FontOptions,
viewport: Viewport,
session: Session,
}
struct Chunk {
values: Rc<ComputedValues>,
x: Option<f64>,
y: Option<f64>,
spans: Vec<Span>,
}
struct MeasuredChunk {
values: Rc<ComputedValues>,
x: Option<f64>,
y: Option<f64>,
dx: f64,
dy: f64,
spans: Vec<MeasuredSpan>,
}
struct PositionedChunk {
next_chunk_x: f64,
next_chunk_y: f64,
spans: Vec<PositionedSpan>,
}
struct Span {
values: Rc<ComputedValues>,
text: String,
dx: f64,
dy: f64,
_depth: usize,
link_target: Option<String>,
}
struct MeasuredSpan {
values: Rc<ComputedValues>,
layout: pango::Layout,
layout_size: (f64, f64),
advance: (f64, f64),
dx: f64,
dy: f64,
link_target: Option<String>,
}
struct PositionedSpan {
layout: pango::Layout,
values: Rc<ComputedValues>,
rendered_position: (f64, f64),
next_span_position: (f64, f64),
link_target: Option<String>,
}
struct LayoutSpan {
layout: pango::Layout,
gravity: pango::Gravity,
bbox: Option<BoundingBox>,
is_visible: bool,
x: f64,
y: f64,
paint_order: PaintOrder,
stroke: Stroke,
stroke_paint: Rc<PaintSource>,
fill_paint: Rc<PaintSource>,
text_rendering: TextRendering,
link_target: Option<String>,
values: Rc<ComputedValues>,
}
impl Chunk {
fn new(values: &ComputedValues, x: Option<f64>, y: Option<f64>) -> Chunk {
Chunk {
values: Rc::new(values.clone()),
x,
y,
spans: Vec::new(),
}
}
}
impl MeasuredChunk {
fn from_chunk(layout_context: &LayoutContext, chunk: &Chunk) -> MeasuredChunk {
let mut measured_spans: Vec<MeasuredSpan> = chunk
.spans
.iter()
.filter_map(|span| MeasuredSpan::from_span(layout_context, span))
.collect();
let (chunk_dx, chunk_dy) = if let Some(first) = measured_spans.first_mut() {
let dx = first.dx;
let dy = first.dy;
first.dx = 0.0;
first.dy = 0.0;
(dx, dy)
} else {
(0.0, 0.0)
};
MeasuredChunk {
values: chunk.values.clone(),
x: chunk.x,
y: chunk.y,
dx: chunk_dx,
dy: chunk_dy,
spans: measured_spans,
}
}
}
impl PositionedChunk {
fn from_measured(
layout_context: &LayoutContext,
measured: &MeasuredChunk,
chunk_x: f64,
chunk_y: f64,
) -> PositionedChunk {
let chunk_direction = measured.values.direction();
let mut positioned = Vec::new();
let mut x = 0.0;
let mut y = 0.0;
let mut chunk_bounds: Option<Rect> = None;
for mspan in &measured.spans {
let params = NormalizeParams::new(&mspan.values, &layout_context.viewport);
let layout = mspan.layout.clone();
let layout_size = mspan.layout_size;
let values = mspan.values.clone();
let dx = mspan.dx;
let dy = mspan.dy;
let advance = mspan.advance;
let baseline_offset = compute_baseline_offset(&layout, &values, ¶ms);
let start_pos = match chunk_direction {
Direction::Ltr => (x, y),
Direction::Rtl => (x - advance.0, y),
};
let span_advance = match chunk_direction {
Direction::Ltr => (advance.0, advance.1),
Direction::Rtl => (-advance.0, advance.1),
};
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)
};
let span_bounds =
Rect::from_size(layout_size.0, layout_size.1).translate(rendered_position);
if let Some(bounds) = chunk_bounds {
chunk_bounds = Some(bounds.union(&span_bounds));
} else {
chunk_bounds = Some(span_bounds);
}
x = x + span_advance.0 + dx;
y = y + span_advance.1 + dy;
let positioned_span = PositionedSpan {
layout,
values,
rendered_position,
next_span_position: (x, y),
link_target: mspan.link_target.clone(),
};
positioned.push(positioned_span);
}
let anchor_offset = text_anchor_offset(
measured.values.text_anchor(),
chunk_direction,
layout_context.writing_mode,
chunk_bounds.unwrap_or_default(),
);
let mut next_chunk_x = chunk_x;
let mut next_chunk_y = chunk_y;
for pspan in &mut positioned {
pspan.rendered_position.0 += chunk_x + anchor_offset.0 + measured.dx;
pspan.rendered_position.1 += chunk_y + anchor_offset.1 + measured.dy;
next_chunk_x = chunk_x + pspan.next_span_position.0 + anchor_offset.0 + measured.dx;
next_chunk_y = chunk_y + pspan.next_span_position.1 + anchor_offset.1 + measured.dy;
}
PositionedChunk {
next_chunk_x,
next_chunk_y,
spans: positioned,
}
}
}
fn compute_baseline_offset(
layout: &pango::Layout,
values: &ComputedValues,
params: &NormalizeParams,
) -> f64 {
let baseline = f64::from(layout.baseline()) / f64::from(pango::SCALE);
let baseline_shift = values.baseline_shift().0.to_user(params);
baseline + baseline_shift
}
#[rustfmt::skip]
fn text_anchor_offset(
anchor: TextAnchor,
direction: Direction,
writing_mode: WritingMode,
chunk_bounds: Rect,
) -> (f64, f64) {
let (w, h) = (chunk_bounds.width(), chunk_bounds.height());
let x0 = chunk_bounds.x0;
if writing_mode.is_horizontal() {
match (anchor, direction) {
(TextAnchor::Start, Direction::Ltr) => (-x0, 0.0),
(TextAnchor::Start, Direction::Rtl) => (-x0 - w, 0.0),
(TextAnchor::Middle, Direction::Ltr) => (-x0 - w / 2.0, 0.0),
(TextAnchor::Middle, Direction::Rtl) => (-x0 - w / 2.0, 0.0),
(TextAnchor::End, Direction::Ltr) => (-x0 - w, 0.0),
(TextAnchor::End, Direction::Rtl) => (-x0, 0.0),
}
} else {
match anchor {
TextAnchor::Start => (0.0, 0.0),
TextAnchor::Middle => (0.0, -h / 2.0),
TextAnchor::End => (0.0, -h),
}
}
}
impl Span {
fn new(
text: &str,
values: Rc<ComputedValues>,
dx: f64,
dy: f64,
depth: usize,
link_target: Option<String>,
) -> Span {
Span {
values,
text: text.to_string(),
dx,
dy,
_depth: depth,
link_target,
}
}
}
struct PangoUnits(i32);
impl PangoUnits {
fn from_pixels(v: f64) -> Option<Self> {
cast::i32(v * f64::from(pango::SCALE) + 0.5)
.ok()
.map(PangoUnits)
}
}
impl MeasuredSpan {
fn from_span(layout_context: &LayoutContext, span: &Span) -> Option<MeasuredSpan> {
let values = span.values.clone();
let params = NormalizeParams::new(&values, &layout_context.viewport);
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);
if let Some(layout) = create_pango_layout(layout_context, &properties, &with_control_chars)
{
let (w, h) = layout.size();
let w = f64::from(w) / f64::from(pango::SCALE);
let h = f64::from(h) / f64::from(pango::SCALE);
let advance = if layout_context.writing_mode.is_horizontal() {
(w, 0.0)
} else {
(0.0, w)
};
Some(MeasuredSpan {
values,
layout,
layout_size: (w, h),
advance,
dx: span.dx,
dy: span.dy,
link_target: span.link_target.clone(),
})
} else {
None
}
}
}
fn gravity_is_vertical(gravity: pango::Gravity) -> bool {
matches!(gravity, pango::Gravity::East | pango::Gravity::West)
}
fn compute_text_box(
layout: &pango::Layout,
x: f64,
y: f64,
transform: Transform,
gravity: pango::Gravity,
) -> Option<BoundingBox> {
#![allow(clippy::many_single_char_names)]
let (ink, _) = layout.extents();
if ink.width() == 0 || ink.height() == 0 {
return None;
}
let ink_x = f64::from(ink.x());
let ink_y = f64::from(ink.y());
let ink_width = f64::from(ink.width());
let ink_height = f64::from(ink.height());
let pango_scale = f64::from(pango::SCALE);
let (x, y, w, h) = if gravity_is_vertical(gravity) {
(
x + (ink_x - ink_height) / pango_scale,
y + ink_y / pango_scale,
ink_height / pango_scale,
ink_width / pango_scale,
)
} else {
(
x + ink_x / pango_scale,
y + ink_y / pango_scale,
ink_width / pango_scale,
ink_height / pango_scale,
)
};
let r = Rect::new(x, y, x + w, y + h);
let bbox = BoundingBox::new()
.with_transform(transform)
.with_rect(r)
.with_ink_rect(r);
Some(bbox)
}
impl PositionedSpan {
fn layout(
&self,
layout_context: &LayoutContext,
acquired_nodes: &mut AcquiredNodes<'_>,
) -> LayoutSpan {
let params = NormalizeParams::new(&self.values, &layout_context.viewport);
let layout = self.layout.clone();
let is_visible = self.values.is_visible();
let (x, y) = self.rendered_position;
let stroke = Stroke::new(&self.values, ¶ms);
let gravity = layout.context().gravity();
let bbox = compute_text_box(&layout, x, y, *layout_context.transform, gravity);
let stroke_paint = self.values.stroke().0.resolve(
acquired_nodes,
self.values.stroke_opacity().0,
self.values.color().0,
None,
None,
&layout_context.session,
);
let fill_paint = self.values.fill().0.resolve(
acquired_nodes,
self.values.fill_opacity().0,
self.values.color().0,
None,
None,
&layout_context.session,
);
let paint_order = self.values.paint_order();
let text_rendering = self.values.text_rendering();
LayoutSpan {
layout,
gravity,
bbox,
is_visible,
x,
y,
paint_order,
stroke,
stroke_paint,
fill_paint,
text_rendering,
values: self.values.clone(),
link_target: self.link_target.clone(),
}
}
}
fn children_to_chunks(
chunks: &mut Vec<Chunk>,
node: &Node,
acquired_nodes: &mut AcquiredNodes<'_>,
cascaded: &CascadedValues<'_>,
layout_context: &LayoutContext,
dx: f64,
dy: f64,
depth: usize,
link: Option<String>,
) {
let mut dx = dx;
let mut dy = dy;
for child in node.children() {
if child.is_chars() {
let values = cascaded.get();
child.borrow_chars().to_chunks(
&child,
Rc::new(values.clone()),
chunks,
dx,
dy,
depth,
link.clone(),
);
} else {
assert!(child.is_element());
match *child.borrow_element_data() {
ElementData::TSpan(ref tspan) => {
let cascaded = CascadedValues::clone_with_node(cascaded, &child);
tspan.to_chunks(
&child,
acquired_nodes,
&cascaded,
layout_context,
chunks,
dx,
dy,
depth + 1,
link.clone(),
);
}
ElementData::Link(ref link) => {
let tspan = TSpan::default();
let cascaded = CascadedValues::clone_with_node(cascaded, &child);
tspan.to_chunks(
&child,
acquired_nodes,
&cascaded,
layout_context,
chunks,
dx,
dy,
depth + 1,
link.link.clone(),
);
}
ElementData::TRef(ref tref) => {
let cascaded = CascadedValues::clone_with_node(cascaded, &child);
tref.to_chunks(
&child,
acquired_nodes,
&cascaded,
chunks,
depth + 1,
layout_context,
);
}
_ => (),
}
}
dx = 0.0;
dy = 0.0;
}
}
#[derive(Default)]
pub struct Chars {
string: RefCell<String>,
space_normalized: RefCell<Option<String>>,
}
impl Chars {
pub fn new(initial_text: &str) -> Chars {
Chars {
string: RefCell::new(String::from(initial_text)),
space_normalized: RefCell::new(None),
}
}
pub fn is_empty(&self) -> bool {
self.string.borrow().is_empty()
}
pub fn append(&self, s: &str) {
self.string.borrow_mut().push_str(s);
*self.space_normalized.borrow_mut() = None;
}
fn ensure_normalized_string(&self, node: &Node, values: &ComputedValues) {
let mut normalized = self.space_normalized.borrow_mut();
if (*normalized).is_none() {
let mode = match values.xml_space() {
XmlSpace::Default => XmlSpaceNormalize::Default(NormalizeDefault {
has_element_before: node.previous_sibling().is_some(),
has_element_after: node.next_sibling().is_some(),
}),
XmlSpace::Preserve => XmlSpaceNormalize::Preserve,
};
*normalized = Some(xml_space_normalize(mode, &self.string.borrow()));
}
}
fn make_span(
&self,
node: &Node,
values: Rc<ComputedValues>,
dx: f64,
dy: f64,
depth: usize,
link_target: Option<String>,
) -> Span {
self.ensure_normalized_string(node, &values);
Span::new(
self.space_normalized.borrow().as_ref().unwrap(),
values,
dx,
dy,
depth,
link_target,
)
}
fn to_chunks(
&self,
node: &Node,
values: Rc<ComputedValues>,
chunks: &mut [Chunk],
dx: f64,
dy: f64,
depth: usize,
link_target: Option<String>,
) {
let span = self.make_span(node, values, dx, dy, depth, link_target);
let num_chunks = chunks.len();
assert!(num_chunks > 0);
chunks[num_chunks - 1].spans.push(span);
}
pub fn get_string(&self) -> String {
self.string.borrow().clone()
}
}
#[derive(Default)]
pub struct Text {
x: Length<Horizontal>,
y: Length<Vertical>,
dx: Length<Horizontal>,
dy: Length<Vertical>,
}
impl Text {
fn make_chunks(
&self,
node: &Node,
acquired_nodes: &mut AcquiredNodes<'_>,
cascaded: &CascadedValues<'_>,
layout_context: &LayoutContext,
x: f64,
y: f64,
) -> Vec<Chunk> {
let mut chunks = Vec::new();
let values = cascaded.get();
let params = NormalizeParams::new(values, &layout_context.viewport);
chunks.push(Chunk::new(values, Some(x), Some(y)));
let dx = self.dx.to_user(¶ms);
let dy = self.dy.to_user(¶ms);
children_to_chunks(
&mut chunks,
node,
acquired_nodes,
cascaded,
layout_context,
dx,
dy,
0,
None,
);
chunks
}
}
fn parse_list_and_extract_first<T: Copy + Default + Parse>(
dest: &mut T,
attr: QualName,
value: &str,
session: &Session,
) {
let mut list: CommaSeparatedList<T, 0, 1024> = CommaSeparatedList(Vec::new());
set_attribute(&mut list, attr.parse(value), session);
if list.0.is_empty() {
*dest = Default::default();
} else {
*dest = list.0[0]; }
}
impl ElementTrait for Text {
fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
for (attr, value) in attrs.iter() {
match attr.expanded() {
expanded_name!("", "x") => {
parse_list_and_extract_first(&mut self.x, attr, value, session)
}
expanded_name!("", "y") => {
parse_list_and_extract_first(&mut self.y, attr, value, session)
}
expanded_name!("", "dx") => {
parse_list_and_extract_first(&mut self.dx, attr, value, session)
}
expanded_name!("", "dy") => {
parse_list_and_extract_first(&mut self.dy, attr, value, session)
}
_ => (),
}
}
}
fn layout(
&self,
node: &Node,
acquired_nodes: &mut AcquiredNodes<'_>,
cascaded: &CascadedValues<'_>,
viewport: &Viewport,
draw_ctx: &mut DrawingCtx,
_clipping: bool,
) -> Result<Option<Layer>, InternalRenderingError> {
let values = cascaded.get();
let params = NormalizeParams::new(values, viewport);
let elt = node.borrow_element();
let session = draw_ctx.session().clone();
let stacking_ctx = StackingContext::new(
&session,
acquired_nodes,
&elt,
values.transform(),
None,
values,
);
let layout_text = {
let transform = ValidTransform::try_from(Transform::identity()).unwrap();
let layout_context = LayoutContext {
writing_mode: values.writing_mode(),
transform,
font_options: draw_ctx.get_font_options(),
viewport: viewport.clone(),
session: session.clone(),
};
let mut x = self.x.to_user(¶ms);
let mut y = self.y.to_user(¶ms);
let chunks = self.make_chunks(node, acquired_nodes, cascaded, &layout_context, x, y);
let mut measured_chunks = Vec::new();
for chunk in &chunks {
measured_chunks.push(MeasuredChunk::from_chunk(&layout_context, chunk));
}
let mut positioned_chunks = Vec::new();
for chunk in &measured_chunks {
let chunk_x = chunk.x.unwrap_or(x);
let chunk_y = chunk.y.unwrap_or(y);
let positioned =
PositionedChunk::from_measured(&layout_context, chunk, chunk_x, chunk_y);
x = positioned.next_chunk_x;
y = positioned.next_chunk_y;
positioned_chunks.push(positioned);
}
let mut layout_spans = Vec::new();
for chunk in &positioned_chunks {
for span in &chunk.spans {
layout_spans.push(span.layout(&layout_context, acquired_nodes));
}
}
let empty_bbox = BoundingBox::new().with_transform(*transform);
let text_bbox = layout_spans.iter().fold(empty_bbox, |mut bbox, span| {
if let Some(ref span_bbox) = span.bbox {
bbox.insert(span_bbox);
}
bbox
});
let mut text_spans = Vec::new();
for span in layout_spans {
let normalize_values = NormalizeValues::new(&span.values);
let stroke_paint = span.stroke_paint.to_user_space(
&text_bbox.rect,
&layout_context.viewport,
&normalize_values,
);
let fill_paint = span.fill_paint.to_user_space(
&text_bbox.rect,
&layout_context.viewport,
&normalize_values,
);
let text_span = TextSpan {
layout: span.layout,
gravity: span.gravity,
bbox: span.bbox,
is_visible: span.is_visible,
x: span.x,
y: span.y,
paint_order: span.paint_order,
stroke: span.stroke,
stroke_paint,
fill_paint,
text_rendering: span.text_rendering,
link_target: span.link_target,
};
text_spans.push(text_span);
}
layout::Text { spans: text_spans }
};
Ok(Some(Layer {
kind: LayerKind::Text(Box::new(layout_text)),
stacking_ctx,
}))
}
fn draw(
&self,
node: &Node,
acquired_nodes: &mut AcquiredNodes<'_>,
cascaded: &CascadedValues<'_>,
viewport: &Viewport,
draw_ctx: &mut DrawingCtx,
clipping: bool,
) -> Result<BoundingBox, InternalRenderingError> {
self.layout(node, acquired_nodes, cascaded, viewport, draw_ctx, clipping)
.and_then(|layer| {
draw_ctx.draw_layer(layer.as_ref().unwrap(), acquired_nodes, clipping, viewport)
})
}
}
#[derive(Default)]
pub struct TRef {
link: Option<NodeId>,
}
impl TRef {
fn to_chunks(
&self,
node: &Node,
acquired_nodes: &mut AcquiredNodes<'_>,
cascaded: &CascadedValues<'_>,
chunks: &mut Vec<Chunk>,
depth: usize,
layout_context: &LayoutContext,
) {
if self.link.is_none() {
return;
}
let link = self.link.as_ref().unwrap();
let values = cascaded.get();
if !values.is_displayed() {
return;
}
if let Ok(acquired) = acquired_nodes.acquire(link) {
let c = acquired.get();
extract_chars_children_to_chunks_recursively(chunks, c, Rc::new(values.clone()), depth);
} else {
rsvg_log!(
layout_context.session,
"element {} references a nonexistent text source \"{}\"",
node,
link,
);
}
}
}
fn extract_chars_children_to_chunks_recursively(
chunks: &mut Vec<Chunk>,
node: &Node,
values: Rc<ComputedValues>,
depth: usize,
) {
for child in node.children() {
let values = values.clone();
if child.is_chars() {
child
.borrow_chars()
.to_chunks(&child, values, chunks, 0.0, 0.0, depth, None)
} else {
extract_chars_children_to_chunks_recursively(chunks, &child, values, depth + 1)
}
}
}
impl ElementTrait for TRef {
fn set_attributes(&mut self, attrs: &Attributes, _session: &Session) {
self.link = attrs
.iter()
.find(|(attr, _)| attr.expanded() == expanded_name!(xlink "href"))
.and_then(|(attr, value)| NodeId::parse(value).attribute(attr).ok());
}
}
#[derive(Default)]
pub struct TSpan {
x: Option<Length<Horizontal>>,
y: Option<Length<Vertical>>,
dx: Length<Horizontal>,
dy: Length<Vertical>,
}
impl TSpan {
fn to_chunks(
&self,
node: &Node,
acquired_nodes: &mut AcquiredNodes<'_>,
cascaded: &CascadedValues<'_>,
layout_context: &LayoutContext,
chunks: &mut Vec<Chunk>,
dx: f64,
dy: f64,
depth: usize,
link: Option<String>,
) {
let values = cascaded.get();
if !values.is_displayed() {
return;
}
let params = NormalizeParams::new(values, &layout_context.viewport);
let x = self.x.map(|l| l.to_user(¶ms));
let y = self.y.map(|l| l.to_user(¶ms));
let span_dx = dx + self.dx.to_user(¶ms);
let span_dy = dy + self.dy.to_user(¶ms);
if x.is_some() || y.is_some() {
chunks.push(Chunk::new(values, x, y));
}
children_to_chunks(
chunks,
node,
acquired_nodes,
cascaded,
layout_context,
span_dx,
span_dy,
depth,
link,
);
}
}
impl ElementTrait for TSpan {
fn set_attributes(&mut self, attrs: &Attributes, session: &Session) {
for (attr, value) in attrs.iter() {
match attr.expanded() {
expanded_name!("", "x") => {
parse_list_and_extract_first(&mut self.x, attr, value, session)
}
expanded_name!("", "y") => {
parse_list_and_extract_first(&mut self.y, attr, value, session)
}
expanded_name!("", "dx") => {
parse_list_and_extract_first(&mut self.dx, attr, value, session)
}
expanded_name!("", "dy") => {
parse_list_and_extract_first(&mut self.dy, attr, value, session)
}
_ => (),
}
}
}
}
impl From<FontStyle> for pango::Style {
fn from(s: FontStyle) -> pango::Style {
match s {
FontStyle::Normal => pango::Style::Normal,
FontStyle::Italic => pango::Style::Italic,
FontStyle::Oblique => pango::Style::Oblique,
}
}
}
impl From<FontVariant> for pango::Variant {
fn from(v: FontVariant) -> pango::Variant {
match v {
FontVariant::Normal => pango::Variant::Normal,
FontVariant::SmallCaps => pango::Variant::SmallCaps,
}
}
}
impl From<FontStretch> for pango::Stretch {
fn from(s: FontStretch) -> pango::Stretch {
match s {
FontStretch::Normal => pango::Stretch::Normal,
FontStretch::Wider => pango::Stretch::Expanded, FontStretch::Narrower => pango::Stretch::Condensed, FontStretch::UltraCondensed => pango::Stretch::UltraCondensed,
FontStretch::ExtraCondensed => pango::Stretch::ExtraCondensed,
FontStretch::Condensed => pango::Stretch::Condensed,
FontStretch::SemiCondensed => pango::Stretch::SemiCondensed,
FontStretch::SemiExpanded => pango::Stretch::SemiExpanded,
FontStretch::Expanded => pango::Stretch::Expanded,
FontStretch::ExtraExpanded => pango::Stretch::ExtraExpanded,
FontStretch::UltraExpanded => pango::Stretch::UltraExpanded,
}
}
}
impl From<FontWeight> for pango::Weight {
fn from(w: FontWeight) -> pango::Weight {
pango::Weight::__Unknown(w.numeric_weight().into())
}
}
impl From<Direction> for pango::Direction {
fn from(d: Direction) -> pango::Direction {
match d {
Direction::Ltr => pango::Direction::Ltr,
Direction::Rtl => pango::Direction::Rtl,
}
}
}
impl From<WritingMode> for pango::Direction {
fn from(m: WritingMode) -> pango::Direction {
use WritingMode::*;
match m {
HorizontalTb | VerticalRl | VerticalLr | LrTb | Lr | Tb | TbRl => pango::Direction::Ltr,
RlTb | Rl => pango::Direction::Rtl,
}
}
}
impl From<WritingMode> for pango::Gravity {
fn from(m: WritingMode) -> pango::Gravity {
use WritingMode::*;
match m {
HorizontalTb | LrTb | Lr | RlTb | Rl => pango::Gravity::South,
VerticalRl | Tb | TbRl => pango::Gravity::East,
VerticalLr => pango::Gravity::West,
}
}
}
mod directional_formatting_characters {
pub const LRE: char = '\u{202a}';
pub const RLE: char = '\u{202b}';
pub const LRO: char = '\u{202d}';
pub const RLO: char = '\u{202e}';
pub const PDF: char = '\u{202c}';
pub const LRI: char = '\u{2066}';
pub const RLI: char = '\u{2067}';
pub const FSI: char = '\u{2068}';
pub const PDI: char = '\u{2069}';
}
struct BidiControl {
start: &'static [char],
end: &'static [char],
}
impl BidiControl {
#[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 }
}
}
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(
layout_context: &LayoutContext,
props: &FontProperties,
text: &str,
) -> Option<pango::Layout> {
let pango_context =
create_pango_context(&layout_context.font_options, &layout_context.transform);
if let XmlLang(Some(ref lang)) = props.xml_lang {
pango_context.set_language(Some(&pango::Language::from_string(lang.as_str())));
}
pango_context.set_base_gravity(pango::Gravity::from(layout_context.writing_mode));
match (props.unicode_bidi, props.direction) {
(UnicodeBidi::BidiOverride, _) | (UnicodeBidi::Embed, _) => {
pango_context.set_base_dir(pango::Direction::from(props.direction));
}
(_, direction) if direction != Direction::Ltr => {
pango_context.set_base_dir(pango::Direction::from(direction));
}
(_, _) => {
pango_context.set_base_dir(pango::Direction::from(layout_context.writing_mode));
}
}
let layout = pango::Layout::new(&pango_context);
let font_size = PangoUnits::from_pixels(props.font_size);
let letter_spacing = PangoUnits::from_pixels(props.letter_spacing);
if font_size.is_none() {
rsvg_log!(
&layout_context.session,
"font-size {} is out of bounds; ignoring span",
props.font_size
);
}
if letter_spacing.is_none() {
rsvg_log!(
&layout_context.session,
"letter-spacing {} is out of bounds; ignoring span",
props.letter_spacing
);
}
if let (Some(font_size), Some(letter_spacing)) = (font_size, letter_spacing) {
let attr_list = pango::AttrList::new();
add_pango_attributes(&attr_list, props, 0, text.len(), font_size, letter_spacing);
layout.set_attributes(Some(&attr_list));
layout.set_text(text);
layout.set_auto_dir(false);
Some(layout)
} else {
None
}
}
fn add_pango_attributes(
attr_list: &pango::AttrList,
props: &FontProperties,
start_index: usize,
end_index: usize,
font_size: PangoUnits,
letter_spacing: PangoUnits,
) {
let start_index = u32::try_from(start_index).expect("Pango attribute index must fit in u32");
let end_index = u32::try_from(end_index).expect("Pango attribute index must fit in u32");
assert!(start_index <= end_index);
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));
font_desc.set_variant(pango::Variant::from(props.font_variant));
font_desc.set_weight(pango::Weight::from(props.font_weight));
font_desc.set_stretch(pango::Stretch::from(props.font_stretch));
font_desc.set_size(font_size.0);
attributes.push(pango::AttrFontDesc::new(&font_desc).upcast());
attributes.push(pango::AttrInt::new_letter_spacing(letter_spacing.0).upcast());
if props.text_decoration.overline {
attributes.push(pango::AttrInt::new_overline(pango::Overline::Single).upcast());
}
if props.text_decoration.underline {
attributes.push(pango::AttrInt::new_underline(pango::Underline::Single).upcast());
}
if props.text_decoration.strike {
attributes.push(pango::AttrInt::new_strikethrough(true).upcast());
}
for attr in &mut attributes {
attr.set_start_index(start_index);
attr.set_end_index(end_index);
}
for attr in attributes {
attr_list.insert(attr);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chars_default() {
let c = Chars::default();
assert!(c.is_empty());
assert!(c.space_normalized.borrow().is_none());
}
#[test]
fn chars_new() {
let example = "Test 123";
let c = Chars::new(example);
assert_eq!(c.get_string(), example);
assert!(c.space_normalized.borrow().is_none());
}
#[test]
fn adjusted_advance_horizontal_ltr() {
use Direction::*;
use TextAnchor::*;
assert_eq!(
text_anchor_offset(
Start,
Ltr,
WritingMode::Lr,
Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
),
(-5.0, 0.0)
);
assert_eq!(
text_anchor_offset(
Middle,
Ltr,
WritingMode::Lr,
Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
),
(-5.5, 0.0)
);
assert_eq!(
text_anchor_offset(
End,
Ltr,
WritingMode::Lr,
Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
),
(-6.0, 0.0)
);
}
#[test]
fn adjusted_advance_horizontal_rtl() {
use Direction::*;
use TextAnchor::*;
assert_eq!(
text_anchor_offset(
Start,
Rtl,
WritingMode::Rl,
Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
),
(-6.0, 0.0)
);
assert_eq!(
text_anchor_offset(
Middle,
Rtl,
WritingMode::Rl,
Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
),
(-5.5, 0.0)
);
assert_eq!(
text_anchor_offset(
TextAnchor::End,
Direction::Rtl,
WritingMode::Rl,
Rect::from_size(1.0, 2.0).translate((5.0, 6.0))
),
(-5.0, 0.0)
);
}
#[test]
fn adjusted_advance_vertical() {
use Direction::*;
use TextAnchor::*;
assert_eq!(
text_anchor_offset(Start, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)),
(0.0, 0.0)
);
assert_eq!(
text_anchor_offset(Middle, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)),
(0.0, -2.0)
);
assert_eq!(
text_anchor_offset(End, Ltr, WritingMode::Tb, Rect::from_size(2.0, 4.0)),
(0.0, -4.0)
);
}
#[test]
fn pango_units_works() {
assert_eq!(PangoUnits::from_pixels(10.0).unwrap().0, pango::SCALE * 10);
}
#[test]
fn pango_units_detects_overflow() {
assert!(PangoUnits::from_pixels(1e7).is_none());
}
}