| 1 | // Copyright 2022 the Resvg Authors |
| 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
| 3 | |
| 4 | use std::collections::HashMap; |
| 5 | use std::num::NonZeroU16; |
| 6 | use std::sync::Arc; |
| 7 | |
| 8 | use fontdb::{Database, ID}; |
| 9 | use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv}; |
| 10 | use rustybuzz::ttf_parser; |
| 11 | use rustybuzz::ttf_parser::{GlyphId, Tag}; |
| 12 | use strict_num::NonZeroPositiveF32; |
| 13 | use tiny_skia_path::{NonZeroRect, Transform}; |
| 14 | use unicode_script::UnicodeScript; |
| 15 | |
| 16 | use crate::tree::{BBox, IsValidLength}; |
| 17 | use crate::{ |
| 18 | AlignmentBaseline, ApproxZeroUlps, BaselineShift, DominantBaseline, Fill, FillRule, Font, |
| 19 | FontResolver, LengthAdjust, PaintOrder, Path, ShapeRendering, Stroke, Text, TextAnchor, |
| 20 | TextChunk, TextDecorationStyle, TextFlow, TextPath, TextSpan, WritingMode, |
| 21 | }; |
| 22 | |
| 23 | /// A glyph that has already been positioned correctly. |
| 24 | /// |
| 25 | /// Note that the transform already takes the font size into consideration, so applying the |
| 26 | /// transform to the outline of the glyphs is all that is necessary to display it correctly. |
| 27 | #[derive (Clone, Debug)] |
| 28 | pub struct PositionedGlyph { |
| 29 | /// Returns the transform of the glyph itself within the cluster. For example, |
| 30 | /// for zalgo text, it contains the transform to position the glyphs above/below |
| 31 | /// the main glyph. |
| 32 | glyph_ts: Transform, |
| 33 | /// Returns the transform of the whole cluster that the glyph is part of. |
| 34 | cluster_ts: Transform, |
| 35 | /// Returns the transform of the span that the glyph is a part of. |
| 36 | span_ts: Transform, |
| 37 | /// The units per em of the font the glyph belongs to. |
| 38 | units_per_em: u16, |
| 39 | /// The font size the glyph should be scaled to. |
| 40 | font_size: f32, |
| 41 | /// The ID of the glyph. |
| 42 | pub id: GlyphId, |
| 43 | /// The text from the original string that corresponds to that glyph. |
| 44 | pub text: String, |
| 45 | /// The ID of the font the glyph should be taken from. Can be used with the |
| 46 | /// [font database of the tree](crate::Tree::fontdb) this glyph is part of. |
| 47 | pub font: ID, |
| 48 | } |
| 49 | |
| 50 | impl PositionedGlyph { |
| 51 | /// Returns the transform of glyph. |
| 52 | pub fn transform(&self) -> Transform { |
| 53 | let sx = self.font_size / self.units_per_em as f32; |
| 54 | |
| 55 | self.span_ts |
| 56 | .pre_concat(self.cluster_ts) |
| 57 | .pre_concat(Transform::from_scale(sx, sx)) |
| 58 | .pre_concat(self.glyph_ts) |
| 59 | } |
| 60 | |
| 61 | /// Returns the transform of glyph, assuming that an outline |
| 62 | /// glyph is being used (i.e. from the `glyf` or `CFF/CFF2` table). |
| 63 | pub fn outline_transform(&self) -> Transform { |
| 64 | // Outlines are mirrored by default. |
| 65 | self.transform() |
| 66 | .pre_concat(Transform::from_scale(1.0, -1.0)) |
| 67 | } |
| 68 | |
| 69 | /// Returns the transform for the glyph, assuming that a CBTD-based raster glyph |
| 70 | /// is being used. |
| 71 | pub fn cbdt_transform(&self, x: f32, y: f32, pixels_per_em: f32, height: f32) -> Transform { |
| 72 | self.transform() |
| 73 | .pre_concat(Transform::from_scale( |
| 74 | self.units_per_em as f32 / pixels_per_em, |
| 75 | self.units_per_em as f32 / pixels_per_em, |
| 76 | )) |
| 77 | // Right now, the top-left corner of the image would be placed in |
| 78 | // on the "text cursor", but we want the bottom-left corner to be there, |
| 79 | // so we need to shift it up and also apply the x/y offset. |
| 80 | .pre_translate(x, -height - y) |
| 81 | } |
| 82 | |
| 83 | /// Returns the transform for the glyph, assuming that a sbix-based raster glyph |
| 84 | /// is being used. |
| 85 | pub fn sbix_transform( |
| 86 | &self, |
| 87 | x: f32, |
| 88 | y: f32, |
| 89 | x_min: f32, |
| 90 | y_min: f32, |
| 91 | pixels_per_em: f32, |
| 92 | height: f32, |
| 93 | ) -> Transform { |
| 94 | // In contrast to CBDT, we also need to look at the outline bbox of the glyph and add a shift if necessary. |
| 95 | let bbox_x_shift = -x_min; |
| 96 | |
| 97 | let bbox_y_shift = if y_min.approx_zero_ulps(4) { |
| 98 | // For unknown reasons, using Apple Color Emoji will lead to a vertical shift on MacOS, but this shift |
| 99 | // doesn't seem to be coming from the font and most likely is somehow hardcoded. On Windows, |
| 100 | // this shift will not be applied. However, if this shift is not applied the emojis are a bit |
| 101 | // too high up when being together with other text, so we try to imitate this. |
| 102 | // See also https://github.com/harfbuzz/harfbuzz/issues/2679#issuecomment-1345595425 |
| 103 | // So whenever the y-shift is 0, we approximate this vertical shift that seems to be produced by it. |
| 104 | // This value seems to be pretty close to what is happening on MacOS. |
| 105 | // We can still remove this if it turns out to be a problem, but Apple Color Emoji is pretty |
| 106 | // much the only `sbix` font out there and they all seem to have a y-shift of 0, so it |
| 107 | // makes sense to keep it. |
| 108 | 0.128 * self.units_per_em as f32 |
| 109 | } else { |
| 110 | -y_min |
| 111 | }; |
| 112 | |
| 113 | self.transform() |
| 114 | .pre_concat(Transform::from_translate(bbox_x_shift, bbox_y_shift)) |
| 115 | .pre_concat(Transform::from_scale( |
| 116 | self.units_per_em as f32 / pixels_per_em, |
| 117 | self.units_per_em as f32 / pixels_per_em, |
| 118 | )) |
| 119 | // Right now, the top-left corner of the image would be placed in |
| 120 | // on the "text cursor", but we want the bottom-left corner to be there, |
| 121 | // so we need to shift it up and also apply the x/y offset. |
| 122 | .pre_translate(x, -height - y) |
| 123 | } |
| 124 | |
| 125 | /// Returns the transform for the glyph, assuming that an SVG glyph is |
| 126 | /// being used. |
| 127 | pub fn svg_transform(&self) -> Transform { |
| 128 | self.transform() |
| 129 | } |
| 130 | |
| 131 | /// Returns the transform for the glyph, assuming that a COLR glyph is |
| 132 | /// being used. |
| 133 | pub fn colr_transform(&self) -> Transform { |
| 134 | self.outline_transform() |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | /// A span contains a number of layouted glyphs that share the same fill, stroke, paint order and |
| 139 | /// visibility. |
| 140 | #[derive (Clone, Debug)] |
| 141 | pub struct Span { |
| 142 | /// The fill of the span. |
| 143 | pub fill: Option<Fill>, |
| 144 | /// The stroke of the span. |
| 145 | pub stroke: Option<Stroke>, |
| 146 | /// The paint order of the span. |
| 147 | pub paint_order: PaintOrder, |
| 148 | /// The font size of the span. |
| 149 | pub font_size: NonZeroPositiveF32, |
| 150 | /// The visibility of the span. |
| 151 | pub visible: bool, |
| 152 | /// The glyphs that make up the span. |
| 153 | pub positioned_glyphs: Vec<PositionedGlyph>, |
| 154 | /// An underline text decoration of the span. |
| 155 | /// Needs to be rendered before all glyphs. |
| 156 | pub underline: Option<Path>, |
| 157 | /// An overline text decoration of the span. |
| 158 | /// Needs to be rendered before all glyphs. |
| 159 | pub overline: Option<Path>, |
| 160 | /// A line-through text decoration of the span. |
| 161 | /// Needs to be rendered after all glyphs. |
| 162 | pub line_through: Option<Path>, |
| 163 | } |
| 164 | |
| 165 | #[derive (Clone, Debug)] |
| 166 | struct GlyphCluster { |
| 167 | byte_idx: ByteIndex, |
| 168 | codepoint: char, |
| 169 | width: f32, |
| 170 | advance: f32, |
| 171 | ascent: f32, |
| 172 | descent: f32, |
| 173 | has_relative_shift: bool, |
| 174 | glyphs: Vec<PositionedGlyph>, |
| 175 | transform: Transform, |
| 176 | path_transform: Transform, |
| 177 | visible: bool, |
| 178 | } |
| 179 | |
| 180 | impl GlyphCluster { |
| 181 | pub(crate) fn height(&self) -> f32 { |
| 182 | self.ascent - self.descent |
| 183 | } |
| 184 | |
| 185 | pub(crate) fn transform(&self) -> Transform { |
| 186 | self.path_transform.post_concat(self.transform) |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | pub(crate) fn layout_text( |
| 191 | text_node: &Text, |
| 192 | resolver: &FontResolver, |
| 193 | fontdb: &mut Arc<fontdb::Database>, |
| 194 | ) -> Option<(Vec<Span>, NonZeroRect)> { |
| 195 | let mut fonts_cache: FontsCache = HashMap::new(); |
| 196 | |
| 197 | for chunk in &text_node.chunks { |
| 198 | for span in &chunk.spans { |
| 199 | if !fonts_cache.contains_key(&span.font) { |
| 200 | if let Some(font) = |
| 201 | (resolver.select_font)(&span.font, fontdb).and_then(|id| fontdb.load_font(id)) |
| 202 | { |
| 203 | fonts_cache.insert(span.font.clone(), Arc::new(font)); |
| 204 | } |
| 205 | } |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | let mut spans = vec![]; |
| 210 | let mut char_offset = 0; |
| 211 | let mut last_x = 0.0; |
| 212 | let mut last_y = 0.0; |
| 213 | let mut bbox = BBox::default(); |
| 214 | for chunk in &text_node.chunks { |
| 215 | let (x, y) = match chunk.text_flow { |
| 216 | TextFlow::Linear => (chunk.x.unwrap_or(last_x), chunk.y.unwrap_or(last_y)), |
| 217 | TextFlow::Path(_) => (0.0, 0.0), |
| 218 | }; |
| 219 | |
| 220 | let mut clusters = process_chunk(chunk, &fonts_cache, resolver, fontdb); |
| 221 | if clusters.is_empty() { |
| 222 | char_offset += chunk.text.chars().count(); |
| 223 | continue; |
| 224 | } |
| 225 | |
| 226 | apply_writing_mode(text_node.writing_mode, &mut clusters); |
| 227 | apply_letter_spacing(chunk, &mut clusters); |
| 228 | apply_word_spacing(chunk, &mut clusters); |
| 229 | |
| 230 | apply_length_adjust(chunk, &mut clusters); |
| 231 | let mut curr_pos = resolve_clusters_positions( |
| 232 | text_node, |
| 233 | chunk, |
| 234 | char_offset, |
| 235 | text_node.writing_mode, |
| 236 | &fonts_cache, |
| 237 | &mut clusters, |
| 238 | ); |
| 239 | |
| 240 | let mut text_ts = Transform::default(); |
| 241 | if text_node.writing_mode == WritingMode::TopToBottom { |
| 242 | if let TextFlow::Linear = chunk.text_flow { |
| 243 | text_ts = text_ts.pre_rotate_at(90.0, x, y); |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | for span in &chunk.spans { |
| 248 | let font = match fonts_cache.get(&span.font) { |
| 249 | Some(v) => v, |
| 250 | None => continue, |
| 251 | }; |
| 252 | |
| 253 | let decoration_spans = collect_decoration_spans(span, &clusters); |
| 254 | |
| 255 | let mut span_ts = text_ts; |
| 256 | span_ts = span_ts.pre_translate(x, y); |
| 257 | if let TextFlow::Linear = chunk.text_flow { |
| 258 | let shift = resolve_baseline(span, font, text_node.writing_mode); |
| 259 | |
| 260 | // In case of a horizontal flow, shift transform and not clusters, |
| 261 | // because clusters can be rotated and an additional shift will lead |
| 262 | // to invalid results. |
| 263 | span_ts = span_ts.pre_translate(0.0, shift); |
| 264 | } |
| 265 | |
| 266 | let mut underline = None; |
| 267 | let mut overline = None; |
| 268 | let mut line_through = None; |
| 269 | |
| 270 | if let Some(decoration) = span.decoration.underline.clone() { |
| 271 | // TODO: No idea what offset should be used for top-to-bottom layout. |
| 272 | // There is |
| 273 | // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property |
| 274 | // but it doesn't go into details. |
| 275 | let offset = match text_node.writing_mode { |
| 276 | WritingMode::LeftToRight => -font.underline_position(span.font_size.get()), |
| 277 | WritingMode::TopToBottom => font.height(span.font_size.get()) / 2.0, |
| 278 | }; |
| 279 | |
| 280 | if let Some(path) = |
| 281 | convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) |
| 282 | { |
| 283 | bbox = bbox.expand(path.data.bounds()); |
| 284 | underline = Some(path); |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | if let Some(decoration) = span.decoration.overline.clone() { |
| 289 | let offset = match text_node.writing_mode { |
| 290 | WritingMode::LeftToRight => -font.ascent(span.font_size.get()), |
| 291 | WritingMode::TopToBottom => -font.height(span.font_size.get()) / 2.0, |
| 292 | }; |
| 293 | |
| 294 | if let Some(path) = |
| 295 | convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) |
| 296 | { |
| 297 | bbox = bbox.expand(path.data.bounds()); |
| 298 | overline = Some(path); |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | if let Some(decoration) = span.decoration.line_through.clone() { |
| 303 | let offset = match text_node.writing_mode { |
| 304 | WritingMode::LeftToRight => -font.line_through_position(span.font_size.get()), |
| 305 | WritingMode::TopToBottom => 0.0, |
| 306 | }; |
| 307 | |
| 308 | if let Some(path) = |
| 309 | convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) |
| 310 | { |
| 311 | bbox = bbox.expand(path.data.bounds()); |
| 312 | line_through = Some(path); |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | let mut fill = span.fill.clone(); |
| 317 | if let Some(ref mut fill) = fill { |
| 318 | // The `fill-rule` should be ignored. |
| 319 | // https://www.w3.org/TR/SVG2/text.html#TextRenderingOrder |
| 320 | // |
| 321 | // 'Since the fill-rule property does not apply to SVG text elements, |
| 322 | // the specific order of the subpaths within the equivalent path does not matter.' |
| 323 | fill.rule = FillRule::NonZero; |
| 324 | } |
| 325 | |
| 326 | if let Some((span_fragments, span_bbox)) = convert_span(span, &clusters, span_ts) { |
| 327 | bbox = bbox.expand(span_bbox); |
| 328 | |
| 329 | let positioned_glyphs = span_fragments |
| 330 | .into_iter() |
| 331 | .flat_map(|mut gc| { |
| 332 | let cluster_ts = gc.transform(); |
| 333 | gc.glyphs.iter_mut().for_each(|pg| { |
| 334 | pg.cluster_ts = cluster_ts; |
| 335 | pg.span_ts = span_ts; |
| 336 | }); |
| 337 | gc.glyphs |
| 338 | }) |
| 339 | .collect(); |
| 340 | |
| 341 | spans.push(Span { |
| 342 | fill, |
| 343 | stroke: span.stroke.clone(), |
| 344 | paint_order: span.paint_order, |
| 345 | font_size: span.font_size, |
| 346 | visible: span.visible, |
| 347 | positioned_glyphs, |
| 348 | underline, |
| 349 | overline, |
| 350 | line_through, |
| 351 | }); |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | char_offset += chunk.text.chars().count(); |
| 356 | |
| 357 | if text_node.writing_mode == WritingMode::TopToBottom { |
| 358 | if let TextFlow::Linear = chunk.text_flow { |
| 359 | std::mem::swap(&mut curr_pos.0, &mut curr_pos.1); |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | last_x = x + curr_pos.0; |
| 364 | last_y = y + curr_pos.1; |
| 365 | } |
| 366 | |
| 367 | let bbox = bbox.to_non_zero_rect()?; |
| 368 | |
| 369 | Some((spans, bbox)) |
| 370 | } |
| 371 | |
| 372 | fn convert_span( |
| 373 | span: &TextSpan, |
| 374 | clusters: &[GlyphCluster], |
| 375 | text_ts: Transform, |
| 376 | ) -> Option<(Vec<GlyphCluster>, NonZeroRect)> { |
| 377 | let mut span_clusters = vec![]; |
| 378 | let mut bboxes_builder = tiny_skia_path::PathBuilder::new(); |
| 379 | |
| 380 | for cluster in clusters { |
| 381 | if !cluster.visible { |
| 382 | continue; |
| 383 | } |
| 384 | |
| 385 | if span_contains(span, cluster.byte_idx) { |
| 386 | span_clusters.push(cluster.clone()); |
| 387 | } |
| 388 | |
| 389 | let mut advance = cluster.advance; |
| 390 | if advance <= 0.0 { |
| 391 | advance = 1.0; |
| 392 | } |
| 393 | |
| 394 | // We have to calculate text bbox using font metrics and not glyph shape. |
| 395 | if let Some(r) = NonZeroRect::from_xywh(0.0, -cluster.ascent, advance, cluster.height()) { |
| 396 | if let Some(r) = r.transform(cluster.transform()) { |
| 397 | bboxes_builder.push_rect(r.to_rect()); |
| 398 | } |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | let mut bboxes = bboxes_builder.finish()?; |
| 403 | bboxes = bboxes.transform(text_ts)?; |
| 404 | let bbox = bboxes.compute_tight_bounds()?.to_non_zero_rect()?; |
| 405 | |
| 406 | Some((span_clusters, bbox)) |
| 407 | } |
| 408 | |
| 409 | fn collect_decoration_spans(span: &TextSpan, clusters: &[GlyphCluster]) -> Vec<DecorationSpan> { |
| 410 | let mut spans = Vec::new(); |
| 411 | |
| 412 | let mut started = false; |
| 413 | let mut width = 0.0; |
| 414 | let mut transform = Transform::default(); |
| 415 | |
| 416 | for cluster in clusters { |
| 417 | if span_contains(span, cluster.byte_idx) { |
| 418 | if started && cluster.has_relative_shift { |
| 419 | started = false; |
| 420 | spans.push(DecorationSpan { width, transform }); |
| 421 | } |
| 422 | |
| 423 | if !started { |
| 424 | width = cluster.advance; |
| 425 | started = true; |
| 426 | transform = cluster.transform; |
| 427 | } else { |
| 428 | width += cluster.advance; |
| 429 | } |
| 430 | } else if started { |
| 431 | spans.push(DecorationSpan { width, transform }); |
| 432 | started = false; |
| 433 | } |
| 434 | } |
| 435 | |
| 436 | if started { |
| 437 | spans.push(DecorationSpan { width, transform }); |
| 438 | } |
| 439 | |
| 440 | spans |
| 441 | } |
| 442 | |
| 443 | pub(crate) fn convert_decoration( |
| 444 | dy: f32, |
| 445 | span: &TextSpan, |
| 446 | font: &ResolvedFont, |
| 447 | mut decoration: TextDecorationStyle, |
| 448 | decoration_spans: &[DecorationSpan], |
| 449 | transform: Transform, |
| 450 | ) -> Option<Path> { |
| 451 | debug_assert!(!decoration_spans.is_empty()); |
| 452 | |
| 453 | let thickness = font.underline_thickness(span.font_size.get()); |
| 454 | |
| 455 | let mut builder = tiny_skia_path::PathBuilder::new(); |
| 456 | for dec_span in decoration_spans { |
| 457 | let rect = match NonZeroRect::from_xywh(0.0, -thickness / 2.0, dec_span.width, thickness) { |
| 458 | Some(v) => v, |
| 459 | None => { |
| 460 | log::warn!("a decoration span has a malformed bbox" ); |
| 461 | continue; |
| 462 | } |
| 463 | }; |
| 464 | |
| 465 | let ts = dec_span.transform.pre_translate(0.0, dy); |
| 466 | |
| 467 | let mut path = tiny_skia_path::PathBuilder::from_rect(rect.to_rect()); |
| 468 | path = match path.transform(ts) { |
| 469 | Some(v) => v, |
| 470 | None => continue, |
| 471 | }; |
| 472 | |
| 473 | builder.push_path(&path); |
| 474 | } |
| 475 | |
| 476 | let mut path_data = builder.finish()?; |
| 477 | path_data = path_data.transform(transform)?; |
| 478 | |
| 479 | Path::new( |
| 480 | String::new(), |
| 481 | span.visible, |
| 482 | decoration.fill.take(), |
| 483 | decoration.stroke.take(), |
| 484 | PaintOrder::default(), |
| 485 | ShapeRendering::default(), |
| 486 | Arc::new(path_data), |
| 487 | Transform::default(), |
| 488 | ) |
| 489 | } |
| 490 | |
| 491 | /// A text decoration span. |
| 492 | /// |
| 493 | /// Basically a horizontal line, that will be used for underline, overline and line-through. |
| 494 | /// It doesn't have a height, since it depends on the Font metrics. |
| 495 | #[derive (Clone, Copy)] |
| 496 | pub(crate) struct DecorationSpan { |
| 497 | pub(crate) width: f32, |
| 498 | pub(crate) transform: Transform, |
| 499 | } |
| 500 | |
| 501 | /// Resolves clusters positions. |
| 502 | /// |
| 503 | /// Mainly sets the `transform` property. |
| 504 | /// |
| 505 | /// Returns the last text position. The next text chunk should start from that position. |
| 506 | fn resolve_clusters_positions( |
| 507 | text: &Text, |
| 508 | chunk: &TextChunk, |
| 509 | char_offset: usize, |
| 510 | writing_mode: WritingMode, |
| 511 | fonts_cache: &FontsCache, |
| 512 | clusters: &mut [GlyphCluster], |
| 513 | ) -> (f32, f32) { |
| 514 | match chunk.text_flow { |
| 515 | TextFlow::Linear => { |
| 516 | resolve_clusters_positions_horizontal(text, chunk, char_offset, writing_mode, clusters) |
| 517 | } |
| 518 | TextFlow::Path(ref path: &Arc) => resolve_clusters_positions_path( |
| 519 | text, |
| 520 | chunk, |
| 521 | char_offset, |
| 522 | path, |
| 523 | writing_mode, |
| 524 | fonts_cache, |
| 525 | clusters, |
| 526 | ), |
| 527 | } |
| 528 | } |
| 529 | |
| 530 | fn clusters_length(clusters: &[GlyphCluster]) -> f32 { |
| 531 | clusters.iter().fold(init:0.0, |w: f32, cluster: &GlyphCluster| w + cluster.advance) |
| 532 | } |
| 533 | |
| 534 | fn resolve_clusters_positions_horizontal( |
| 535 | text: &Text, |
| 536 | chunk: &TextChunk, |
| 537 | offset: usize, |
| 538 | writing_mode: WritingMode, |
| 539 | clusters: &mut [GlyphCluster], |
| 540 | ) -> (f32, f32) { |
| 541 | let mut x = process_anchor(chunk.anchor, clusters_length(clusters)); |
| 542 | let mut y = 0.0; |
| 543 | |
| 544 | for cluster in clusters { |
| 545 | let cp = offset + cluster.byte_idx.code_point_at(&chunk.text); |
| 546 | if let (Some(dx), Some(dy)) = (text.dx.get(cp), text.dy.get(cp)) { |
| 547 | if writing_mode == WritingMode::LeftToRight { |
| 548 | x += dx; |
| 549 | y += dy; |
| 550 | } else { |
| 551 | y -= dx; |
| 552 | x += dy; |
| 553 | } |
| 554 | cluster.has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4); |
| 555 | } |
| 556 | |
| 557 | cluster.transform = cluster.transform.pre_translate(x, y); |
| 558 | |
| 559 | if let Some(angle) = text.rotate.get(cp).cloned() { |
| 560 | if !angle.approx_zero_ulps(4) { |
| 561 | cluster.transform = cluster.transform.pre_rotate(angle); |
| 562 | cluster.has_relative_shift = true; |
| 563 | } |
| 564 | } |
| 565 | |
| 566 | x += cluster.advance; |
| 567 | } |
| 568 | |
| 569 | (x, y) |
| 570 | } |
| 571 | |
| 572 | // Baseline resolving in SVG is a mess. |
| 573 | // Not only it's poorly documented, but as soon as you start mixing |
| 574 | // `dominant-baseline` and `alignment-baseline` each application/browser will produce |
| 575 | // different results. |
| 576 | // |
| 577 | // For now, resvg simply tries to match Chrome's output and not the mythical SVG spec output. |
| 578 | // |
| 579 | // See `alignment_baseline_shift` method comment for more details. |
| 580 | pub(crate) fn resolve_baseline( |
| 581 | span: &TextSpan, |
| 582 | font: &ResolvedFont, |
| 583 | writing_mode: WritingMode, |
| 584 | ) -> f32 { |
| 585 | let mut shift: f32 = -resolve_baseline_shift(&span.baseline_shift, font, font_size:span.font_size.get()); |
| 586 | |
| 587 | // TODO: support vertical layout as well |
| 588 | if writing_mode == WritingMode::LeftToRight { |
| 589 | if span.alignment_baseline == AlignmentBaseline::Auto |
| 590 | || span.alignment_baseline == AlignmentBaseline::Baseline |
| 591 | { |
| 592 | shift += font.dominant_baseline_shift(span.dominant_baseline, font_size:span.font_size.get()); |
| 593 | } else { |
| 594 | shift += font.alignment_baseline_shift(alignment:span.alignment_baseline, font_size:span.font_size.get()); |
| 595 | } |
| 596 | } |
| 597 | |
| 598 | shift |
| 599 | } |
| 600 | |
| 601 | fn resolve_baseline_shift(baselines: &[BaselineShift], font: &ResolvedFont, font_size: f32) -> f32 { |
| 602 | let mut shift: f32 = 0.0; |
| 603 | for baseline: &BaselineShift in baselines.iter().rev() { |
| 604 | match baseline { |
| 605 | BaselineShift::Baseline => {} |
| 606 | BaselineShift::Subscript => shift -= font.subscript_offset(font_size), |
| 607 | BaselineShift::Superscript => shift += font.superscript_offset(font_size), |
| 608 | BaselineShift::Number(n: &f32) => shift += n, |
| 609 | } |
| 610 | } |
| 611 | |
| 612 | shift |
| 613 | } |
| 614 | |
| 615 | fn resolve_clusters_positions_path( |
| 616 | text: &Text, |
| 617 | chunk: &TextChunk, |
| 618 | char_offset: usize, |
| 619 | path: &TextPath, |
| 620 | writing_mode: WritingMode, |
| 621 | fonts_cache: &FontsCache, |
| 622 | clusters: &mut [GlyphCluster], |
| 623 | ) -> (f32, f32) { |
| 624 | let mut last_x = 0.0; |
| 625 | let mut last_y = 0.0; |
| 626 | |
| 627 | let mut dy = 0.0; |
| 628 | |
| 629 | // In the text path mode, chunk's x/y coordinates provide an additional offset along the path. |
| 630 | // The X coordinate is used in a horizontal mode, and Y in vertical. |
| 631 | let chunk_offset = match writing_mode { |
| 632 | WritingMode::LeftToRight => chunk.x.unwrap_or(0.0), |
| 633 | WritingMode::TopToBottom => chunk.y.unwrap_or(0.0), |
| 634 | }; |
| 635 | |
| 636 | let start_offset = |
| 637 | chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters)); |
| 638 | |
| 639 | let normals = collect_normals(text, chunk, clusters, &path.path, char_offset, start_offset); |
| 640 | for (cluster, normal) in clusters.iter_mut().zip(normals) { |
| 641 | let (x, y, angle) = match normal { |
| 642 | Some(normal) => (normal.x, normal.y, normal.angle), |
| 643 | None => { |
| 644 | // Hide clusters that are outside the text path. |
| 645 | cluster.visible = false; |
| 646 | continue; |
| 647 | } |
| 648 | }; |
| 649 | |
| 650 | // We have to break a decoration line for each cluster during text-on-path. |
| 651 | cluster.has_relative_shift = true; |
| 652 | |
| 653 | let orig_ts = cluster.transform; |
| 654 | |
| 655 | // Clusters should be rotated by the x-midpoint x baseline position. |
| 656 | let half_width = cluster.width / 2.0; |
| 657 | cluster.transform = Transform::default(); |
| 658 | cluster.transform = cluster.transform.pre_translate(x - half_width, y); |
| 659 | cluster.transform = cluster.transform.pre_rotate_at(angle, half_width, 0.0); |
| 660 | |
| 661 | let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text); |
| 662 | dy += text.dy.get(cp).cloned().unwrap_or(0.0); |
| 663 | |
| 664 | let baseline_shift = chunk_span_at(chunk, cluster.byte_idx) |
| 665 | .map(|span| { |
| 666 | let font = match fonts_cache.get(&span.font) { |
| 667 | Some(v) => v, |
| 668 | None => return 0.0, |
| 669 | }; |
| 670 | -resolve_baseline(span, font, writing_mode) |
| 671 | }) |
| 672 | .unwrap_or(0.0); |
| 673 | |
| 674 | // Shift only by `dy` since we already applied `dx` |
| 675 | // during offset along the path calculation. |
| 676 | if !dy.approx_zero_ulps(4) || !baseline_shift.approx_zero_ulps(4) { |
| 677 | let shift = kurbo::Vec2::new(0.0, (dy - baseline_shift) as f64); |
| 678 | cluster.transform = cluster |
| 679 | .transform |
| 680 | .pre_translate(shift.x as f32, shift.y as f32); |
| 681 | } |
| 682 | |
| 683 | if let Some(angle) = text.rotate.get(cp).cloned() { |
| 684 | if !angle.approx_zero_ulps(4) { |
| 685 | cluster.transform = cluster.transform.pre_rotate(angle); |
| 686 | } |
| 687 | } |
| 688 | |
| 689 | // The possible `lengthAdjust` transform should be applied after text-on-path positioning. |
| 690 | cluster.transform = cluster.transform.pre_concat(orig_ts); |
| 691 | |
| 692 | last_x = x + cluster.advance; |
| 693 | last_y = y; |
| 694 | } |
| 695 | |
| 696 | (last_x, last_y) |
| 697 | } |
| 698 | |
| 699 | pub(crate) fn process_anchor(a: TextAnchor, text_width: f32) -> f32 { |
| 700 | match a { |
| 701 | TextAnchor::Start => 0.0, // Nothing. |
| 702 | TextAnchor::Middle => -text_width / 2.0, |
| 703 | TextAnchor::End => -text_width, |
| 704 | } |
| 705 | } |
| 706 | |
| 707 | pub(crate) struct PathNormal { |
| 708 | pub(crate) x: f32, |
| 709 | pub(crate) y: f32, |
| 710 | pub(crate) angle: f32, |
| 711 | } |
| 712 | |
| 713 | fn collect_normals( |
| 714 | text: &Text, |
| 715 | chunk: &TextChunk, |
| 716 | clusters: &[GlyphCluster], |
| 717 | path: &tiny_skia_path::Path, |
| 718 | char_offset: usize, |
| 719 | offset: f32, |
| 720 | ) -> Vec<Option<PathNormal>> { |
| 721 | let mut offsets = Vec::with_capacity(clusters.len()); |
| 722 | let mut normals = Vec::with_capacity(clusters.len()); |
| 723 | { |
| 724 | let mut advance = offset; |
| 725 | for cluster in clusters { |
| 726 | // Clusters should be rotated by the x-midpoint x baseline position. |
| 727 | let half_width = cluster.width / 2.0; |
| 728 | |
| 729 | // Include relative position. |
| 730 | let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text); |
| 731 | advance += text.dx.get(cp).cloned().unwrap_or(0.0); |
| 732 | |
| 733 | let offset = advance + half_width; |
| 734 | |
| 735 | // Clusters outside the path have no normals. |
| 736 | if offset < 0.0 { |
| 737 | normals.push(None); |
| 738 | } |
| 739 | |
| 740 | offsets.push(offset as f64); |
| 741 | advance += cluster.advance; |
| 742 | } |
| 743 | } |
| 744 | |
| 745 | let mut prev_mx = path.points()[0].x; |
| 746 | let mut prev_my = path.points()[0].y; |
| 747 | let mut prev_x = prev_mx; |
| 748 | let mut prev_y = prev_my; |
| 749 | |
| 750 | fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez { |
| 751 | let line = kurbo::Line::new( |
| 752 | kurbo::Point::new(px as f64, py as f64), |
| 753 | kurbo::Point::new(x as f64, y as f64), |
| 754 | ); |
| 755 | let p1 = line.eval(0.33); |
| 756 | let p2 = line.eval(0.66); |
| 757 | kurbo::CubicBez { |
| 758 | p0: line.p0, |
| 759 | p1, |
| 760 | p2, |
| 761 | p3: line.p1, |
| 762 | } |
| 763 | } |
| 764 | |
| 765 | let mut length: f64 = 0.0; |
| 766 | for seg in path.segments() { |
| 767 | let curve = match seg { |
| 768 | tiny_skia_path::PathSegment::MoveTo(p) => { |
| 769 | prev_mx = p.x; |
| 770 | prev_my = p.y; |
| 771 | prev_x = p.x; |
| 772 | prev_y = p.y; |
| 773 | continue; |
| 774 | } |
| 775 | tiny_skia_path::PathSegment::LineTo(p) => { |
| 776 | create_curve_from_line(prev_x, prev_y, p.x, p.y) |
| 777 | } |
| 778 | tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez { |
| 779 | p0: kurbo::Point::new(prev_x as f64, prev_y as f64), |
| 780 | p1: kurbo::Point::new(p1.x as f64, p1.y as f64), |
| 781 | p2: kurbo::Point::new(p.x as f64, p.y as f64), |
| 782 | } |
| 783 | .raise(), |
| 784 | tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez { |
| 785 | p0: kurbo::Point::new(prev_x as f64, prev_y as f64), |
| 786 | p1: kurbo::Point::new(p1.x as f64, p1.y as f64), |
| 787 | p2: kurbo::Point::new(p2.x as f64, p2.y as f64), |
| 788 | p3: kurbo::Point::new(p.x as f64, p.y as f64), |
| 789 | }, |
| 790 | tiny_skia_path::PathSegment::Close => { |
| 791 | create_curve_from_line(prev_x, prev_y, prev_mx, prev_my) |
| 792 | } |
| 793 | }; |
| 794 | |
| 795 | let arclen_accuracy = { |
| 796 | let base_arclen_accuracy = 0.5; |
| 797 | // Accuracy depends on a current scale. |
| 798 | // When we have a tiny path scaled by a large value, |
| 799 | // we have to increase out accuracy accordingly. |
| 800 | let (sx, sy) = text.abs_transform.get_scale(); |
| 801 | // 1.0 acts as a threshold to prevent division by 0 and/or low accuracy. |
| 802 | base_arclen_accuracy / (sx * sy).sqrt().max(1.0) |
| 803 | }; |
| 804 | |
| 805 | let curve_len = curve.arclen(arclen_accuracy as f64); |
| 806 | |
| 807 | for offset in &offsets[normals.len()..] { |
| 808 | if *offset >= length && *offset <= length + curve_len { |
| 809 | let mut offset = curve.inv_arclen(offset - length, arclen_accuracy as f64); |
| 810 | // some rounding error may occur, so we give offset a little tolerance |
| 811 | debug_assert!((-1.0e-3..=1.0 + 1.0e-3).contains(&offset)); |
| 812 | offset = offset.clamp(0.0, 1.0); |
| 813 | |
| 814 | let pos = curve.eval(offset); |
| 815 | let d = curve.deriv().eval(offset); |
| 816 | let d = kurbo::Vec2::new(-d.y, d.x); // tangent |
| 817 | let angle = d.atan2().to_degrees() - 90.0; |
| 818 | |
| 819 | normals.push(Some(PathNormal { |
| 820 | x: pos.x as f32, |
| 821 | y: pos.y as f32, |
| 822 | angle: angle as f32, |
| 823 | })); |
| 824 | |
| 825 | if normals.len() == offsets.len() { |
| 826 | break; |
| 827 | } |
| 828 | } |
| 829 | } |
| 830 | |
| 831 | length += curve_len; |
| 832 | prev_x = curve.p3.x as f32; |
| 833 | prev_y = curve.p3.y as f32; |
| 834 | } |
| 835 | |
| 836 | // If path ended and we still have unresolved normals - set them to `None`. |
| 837 | for _ in 0..(offsets.len() - normals.len()) { |
| 838 | normals.push(None); |
| 839 | } |
| 840 | |
| 841 | normals |
| 842 | } |
| 843 | |
| 844 | /// Converts a text chunk into a list of outlined clusters. |
| 845 | /// |
| 846 | /// This function will do the BIDI reordering, text shaping and glyphs outlining, |
| 847 | /// but not the text layouting. So all clusters are in the 0x0 position. |
| 848 | fn process_chunk( |
| 849 | chunk: &TextChunk, |
| 850 | fonts_cache: &FontsCache, |
| 851 | resolver: &FontResolver, |
| 852 | fontdb: &mut Arc<fontdb::Database>, |
| 853 | ) -> Vec<GlyphCluster> { |
| 854 | // The way this function works is a bit tricky. |
| 855 | // |
| 856 | // The first problem is BIDI reordering. |
| 857 | // We cannot shape text span-by-span, because glyph clusters are not guarantee to be continuous. |
| 858 | // |
| 859 | // For example: |
| 860 | // <text>Hel<tspan fill="url(#lg1)">lo של</tspan>ום.</text> |
| 861 | // |
| 862 | // Would be shaped as: |
| 863 | // H e l l o ש ל ו ם . (characters) |
| 864 | // 0 1 2 3 4 5 12 10 8 6 14 (cluster indices in UTF-8) |
| 865 | // --- --- (green span) |
| 866 | // |
| 867 | // As you can see, our continuous `lo של` span was split into two separated one. |
| 868 | // So our 3 spans: black - green - black, become 5 spans: black - green - black - green - black. |
| 869 | // If we shape `Hel`, then `lo של` an then `ום` separately - we would get an incorrect output. |
| 870 | // To properly handle this we simply shape the whole chunk. |
| 871 | // |
| 872 | // But this introduces another issue - what to do when we have multiple fonts? |
| 873 | // The easy solution would be to simply shape text with each font, |
| 874 | // where the first font output is used as a base one and all others overwrite it. |
| 875 | // This way in case of: |
| 876 | // <text font-family="Arial">Hello <tspan font-family="Helvetica">world</tspan></text> |
| 877 | // we would replace Arial glyphs for `world` with Helvetica one. Pretty simple. |
| 878 | // |
| 879 | // Well, it would work most of the time, but not always. |
| 880 | // This is because different fonts can produce different amount of glyphs for the same text. |
| 881 | // The most common example are ligatures. Some fonts can shape `fi` as two glyphs `f` and `i`, |
| 882 | // but some can use `fi` (U+FB01) instead. |
| 883 | // Meaning that during merging we have to overwrite not individual glyphs, but clusters. |
| 884 | |
| 885 | let mut glyphs = Vec::new(); |
| 886 | for span in &chunk.spans { |
| 887 | let font = match fonts_cache.get(&span.font) { |
| 888 | Some(v) => v.clone(), |
| 889 | None => continue, |
| 890 | }; |
| 891 | |
| 892 | let tmp_glyphs = shape_text( |
| 893 | &chunk.text, |
| 894 | font, |
| 895 | span.small_caps, |
| 896 | span.apply_kerning, |
| 897 | resolver, |
| 898 | fontdb, |
| 899 | ); |
| 900 | |
| 901 | // Do nothing with the first run. |
| 902 | if glyphs.is_empty() { |
| 903 | glyphs = tmp_glyphs; |
| 904 | continue; |
| 905 | } |
| 906 | |
| 907 | // Overwrite span's glyphs. |
| 908 | let mut iter = tmp_glyphs.into_iter(); |
| 909 | while let Some(new_glyph) = iter.next() { |
| 910 | if !span_contains(span, new_glyph.byte_idx) { |
| 911 | continue; |
| 912 | } |
| 913 | |
| 914 | let Some(idx) = glyphs.iter().position(|g| g.byte_idx == new_glyph.byte_idx) else { |
| 915 | continue; |
| 916 | }; |
| 917 | |
| 918 | let prev_cluster_len = glyphs[idx].cluster_len; |
| 919 | if prev_cluster_len < new_glyph.cluster_len { |
| 920 | // If the new font represents the same cluster with fewer glyphs |
| 921 | // then remove remaining glyphs. |
| 922 | for _ in 1..new_glyph.cluster_len { |
| 923 | glyphs.remove(idx + 1); |
| 924 | } |
| 925 | } else if prev_cluster_len > new_glyph.cluster_len { |
| 926 | // If the new font represents the same cluster with more glyphs |
| 927 | // then insert them after the current one. |
| 928 | for j in 1..prev_cluster_len { |
| 929 | if let Some(g) = iter.next() { |
| 930 | glyphs.insert(idx + j, g); |
| 931 | } |
| 932 | } |
| 933 | } |
| 934 | |
| 935 | glyphs[idx] = new_glyph; |
| 936 | } |
| 937 | } |
| 938 | |
| 939 | // Convert glyphs to clusters. |
| 940 | let mut clusters = Vec::new(); |
| 941 | for (range, byte_idx) in GlyphClusters::new(&glyphs) { |
| 942 | if let Some(span) = chunk_span_at(chunk, byte_idx) { |
| 943 | clusters.push(form_glyph_clusters( |
| 944 | &glyphs[range], |
| 945 | &chunk.text, |
| 946 | span.font_size.get(), |
| 947 | )); |
| 948 | } |
| 949 | } |
| 950 | |
| 951 | clusters |
| 952 | } |
| 953 | |
| 954 | fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { |
| 955 | let is_horizontal = matches!(chunk.text_flow, TextFlow::Linear); |
| 956 | |
| 957 | for span in &chunk.spans { |
| 958 | let target_width = match span.text_length { |
| 959 | Some(v) => v, |
| 960 | None => continue, |
| 961 | }; |
| 962 | |
| 963 | let mut width = 0.0; |
| 964 | let mut cluster_indexes = Vec::new(); |
| 965 | for i in span.start..span.end { |
| 966 | if let Some(index) = clusters.iter().position(|c| c.byte_idx.value() == i) { |
| 967 | cluster_indexes.push(index); |
| 968 | } |
| 969 | } |
| 970 | // Complex scripts can have multi-codepoint clusters therefore we have to remove duplicates. |
| 971 | cluster_indexes.sort(); |
| 972 | cluster_indexes.dedup(); |
| 973 | |
| 974 | for i in &cluster_indexes { |
| 975 | // Use the original cluster `width` and not `advance`. |
| 976 | // This method essentially discards any `word-spacing` and `letter-spacing`. |
| 977 | width += clusters[*i].width; |
| 978 | } |
| 979 | |
| 980 | if cluster_indexes.is_empty() { |
| 981 | continue; |
| 982 | } |
| 983 | |
| 984 | if span.length_adjust == LengthAdjust::Spacing { |
| 985 | let factor = if cluster_indexes.len() > 1 { |
| 986 | (target_width - width) / (cluster_indexes.len() - 1) as f32 |
| 987 | } else { |
| 988 | 0.0 |
| 989 | }; |
| 990 | |
| 991 | for i in cluster_indexes { |
| 992 | clusters[i].advance = clusters[i].width + factor; |
| 993 | } |
| 994 | } else { |
| 995 | let factor = target_width / width; |
| 996 | // Prevent multiplying by zero. |
| 997 | if factor < 0.001 { |
| 998 | continue; |
| 999 | } |
| 1000 | |
| 1001 | for i in cluster_indexes { |
| 1002 | clusters[i].transform = clusters[i].transform.pre_scale(factor, 1.0); |
| 1003 | |
| 1004 | // Technically just a hack to support the current text-on-path algorithm. |
| 1005 | if !is_horizontal { |
| 1006 | clusters[i].advance *= factor; |
| 1007 | clusters[i].width *= factor; |
| 1008 | } |
| 1009 | } |
| 1010 | } |
| 1011 | } |
| 1012 | } |
| 1013 | |
| 1014 | /// Rotates clusters according to |
| 1015 | /// [Unicode Vertical_Orientation Property](https://www.unicode.org/reports/tr50/tr50-19.html). |
| 1016 | fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [GlyphCluster]) { |
| 1017 | if writing_mode != WritingMode::TopToBottom { |
| 1018 | return; |
| 1019 | } |
| 1020 | |
| 1021 | for cluster in clusters { |
| 1022 | let orientation = unicode_vo::char_orientation(cluster.codepoint); |
| 1023 | if orientation == unicode_vo::Orientation::Upright { |
| 1024 | let mut ts = Transform::default(); |
| 1025 | // Position glyph in the center of vertical axis. |
| 1026 | ts = ts.pre_translate(0.0, (cluster.ascent + cluster.descent) / 2.0); |
| 1027 | // Rotate by 90 degrees in the center. |
| 1028 | ts = ts.pre_rotate_at( |
| 1029 | -90.0, |
| 1030 | cluster.width / 2.0, |
| 1031 | -(cluster.ascent + cluster.descent) / 2.0, |
| 1032 | ); |
| 1033 | |
| 1034 | cluster.path_transform = ts; |
| 1035 | |
| 1036 | // Move "baseline" to the middle and make height equal to width. |
| 1037 | cluster.ascent = cluster.width / 2.0; |
| 1038 | cluster.descent = -cluster.width / 2.0; |
| 1039 | } else { |
| 1040 | // Could not find a spec that explains this, |
| 1041 | // but this is how other applications are shifting the "rotated" characters |
| 1042 | // in the top-to-bottom mode. |
| 1043 | cluster.transform = cluster |
| 1044 | .transform |
| 1045 | .pre_translate(0.0, (cluster.ascent + cluster.descent) / 2.0); |
| 1046 | } |
| 1047 | } |
| 1048 | } |
| 1049 | |
| 1050 | /// Applies the `letter-spacing` property to a text chunk clusters. |
| 1051 | /// |
| 1052 | /// [In the CSS spec](https://www.w3.org/TR/css-text-3/#letter-spacing-property). |
| 1053 | fn apply_letter_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { |
| 1054 | // At least one span should have a non-zero spacing. |
| 1055 | if !chunk |
| 1056 | .spans |
| 1057 | .iter() |
| 1058 | .any(|span| !span.letter_spacing.approx_zero_ulps(4)) |
| 1059 | { |
| 1060 | return; |
| 1061 | } |
| 1062 | |
| 1063 | let num_clusters = clusters.len(); |
| 1064 | for (i, cluster) in clusters.iter_mut().enumerate() { |
| 1065 | // Spacing must be applied only to characters that belongs to the script |
| 1066 | // that supports spacing. |
| 1067 | // We are checking only the first code point, since it should be enough. |
| 1068 | // https://www.w3.org/TR/css-text-3/#cursive-tracking |
| 1069 | let script = cluster.codepoint.script(); |
| 1070 | if script_supports_letter_spacing(script) { |
| 1071 | if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) { |
| 1072 | // A space after the last cluster should be ignored, |
| 1073 | // since it affects the bbox and text alignment. |
| 1074 | if i != num_clusters - 1 { |
| 1075 | cluster.advance += span.letter_spacing; |
| 1076 | } |
| 1077 | |
| 1078 | // If the cluster advance became negative - clear it. |
| 1079 | // This is an UB so we can do whatever we want, and we mimic Chrome's behavior. |
| 1080 | if !cluster.advance.is_valid_length() { |
| 1081 | cluster.width = 0.0; |
| 1082 | cluster.advance = 0.0; |
| 1083 | cluster.glyphs = vec![]; |
| 1084 | } |
| 1085 | } |
| 1086 | } |
| 1087 | } |
| 1088 | } |
| 1089 | |
| 1090 | /// Applies the `word-spacing` property to a text chunk clusters. |
| 1091 | /// |
| 1092 | /// [In the CSS spec](https://www.w3.org/TR/css-text-3/#propdef-word-spacing). |
| 1093 | fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { |
| 1094 | // At least one span should have a non-zero spacing. |
| 1095 | if !chunkIter<'_, TextSpan> |
| 1096 | .spans |
| 1097 | .iter() |
| 1098 | .any(|span: &TextSpan| !span.word_spacing.approx_zero_ulps(4)) |
| 1099 | { |
| 1100 | return; |
| 1101 | } |
| 1102 | |
| 1103 | for cluster: &mut GlyphCluster in clusters { |
| 1104 | if is_word_separator_characters(cluster.codepoint) { |
| 1105 | if let Some(span: &TextSpan) = chunk_span_at(chunk, byte_offset:cluster.byte_idx) { |
| 1106 | // Technically, word spacing 'should be applied half on each |
| 1107 | // side of the character', but it doesn't affect us in any way, |
| 1108 | // so we are ignoring this. |
| 1109 | cluster.advance += span.word_spacing; |
| 1110 | |
| 1111 | // After word spacing, `advance` can be negative. |
| 1112 | } |
| 1113 | } |
| 1114 | } |
| 1115 | } |
| 1116 | |
| 1117 | fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphCluster { |
| 1118 | debug_assert!(!glyphs.is_empty()); |
| 1119 | |
| 1120 | let mut width = 0.0; |
| 1121 | let mut x: f32 = 0.0; |
| 1122 | |
| 1123 | let mut positioned_glyphs = vec![]; |
| 1124 | |
| 1125 | for glyph in glyphs { |
| 1126 | let sx = glyph.font.scale(font_size); |
| 1127 | |
| 1128 | // Apply offset. |
| 1129 | // |
| 1130 | // The first glyph in the cluster will have an offset from 0x0, |
| 1131 | // but the later one will have an offset from the "current position". |
| 1132 | // So we have to keep an advance. |
| 1133 | // TODO: should be done only inside a single text span |
| 1134 | let ts = Transform::from_translate(x + glyph.dx as f32, -glyph.dy as f32); |
| 1135 | |
| 1136 | positioned_glyphs.push(PositionedGlyph { |
| 1137 | glyph_ts: ts, |
| 1138 | // Will be set later. |
| 1139 | cluster_ts: Transform::default(), |
| 1140 | // Will be set later. |
| 1141 | span_ts: Transform::default(), |
| 1142 | units_per_em: glyph.font.units_per_em.get(), |
| 1143 | font_size, |
| 1144 | font: glyph.font.id, |
| 1145 | text: glyph.text.clone(), |
| 1146 | id: glyph.id, |
| 1147 | }); |
| 1148 | |
| 1149 | x += glyph.width as f32; |
| 1150 | |
| 1151 | let glyph_width = glyph.width as f32 * sx; |
| 1152 | if glyph_width > width { |
| 1153 | width = glyph_width; |
| 1154 | } |
| 1155 | } |
| 1156 | |
| 1157 | let byte_idx = glyphs[0].byte_idx; |
| 1158 | let font = glyphs[0].font.clone(); |
| 1159 | GlyphCluster { |
| 1160 | byte_idx, |
| 1161 | codepoint: byte_idx.char_from(text), |
| 1162 | width, |
| 1163 | advance: width, |
| 1164 | ascent: font.ascent(font_size), |
| 1165 | descent: font.descent(font_size), |
| 1166 | has_relative_shift: false, |
| 1167 | transform: Transform::default(), |
| 1168 | path_transform: Transform::default(), |
| 1169 | glyphs: positioned_glyphs, |
| 1170 | visible: true, |
| 1171 | } |
| 1172 | } |
| 1173 | |
| 1174 | pub(crate) trait DatabaseExt { |
| 1175 | fn load_font(&self, id: ID) -> Option<ResolvedFont>; |
| 1176 | fn has_char(&self, id: ID, c: char) -> bool; |
| 1177 | } |
| 1178 | |
| 1179 | impl DatabaseExt for Database { |
| 1180 | #[inline (never)] |
| 1181 | fn load_font(&self, id: ID) -> Option<ResolvedFont> { |
| 1182 | self.with_face_data(id, |data, face_index| -> Option<ResolvedFont> { |
| 1183 | let font = ttf_parser::Face::parse(data, face_index).ok()?; |
| 1184 | |
| 1185 | let units_per_em = NonZeroU16::new(font.units_per_em())?; |
| 1186 | |
| 1187 | let ascent = font.ascender(); |
| 1188 | let descent = font.descender(); |
| 1189 | |
| 1190 | let x_height = font |
| 1191 | .x_height() |
| 1192 | .and_then(|x| u16::try_from(x).ok()) |
| 1193 | .and_then(NonZeroU16::new); |
| 1194 | let x_height = match x_height { |
| 1195 | Some(height) => height, |
| 1196 | None => { |
| 1197 | // If not set - fallback to height * 45%. |
| 1198 | // 45% is what Firefox uses. |
| 1199 | u16::try_from((f32::from(ascent - descent) * 0.45) as i32) |
| 1200 | .ok() |
| 1201 | .and_then(NonZeroU16::new)? |
| 1202 | } |
| 1203 | }; |
| 1204 | |
| 1205 | let line_through = font.strikeout_metrics(); |
| 1206 | let line_through_position = match line_through { |
| 1207 | Some(metrics) => metrics.position, |
| 1208 | None => x_height.get() as i16 / 2, |
| 1209 | }; |
| 1210 | |
| 1211 | let (underline_position, underline_thickness) = match font.underline_metrics() { |
| 1212 | Some(metrics) => { |
| 1213 | let thickness = u16::try_from(metrics.thickness) |
| 1214 | .ok() |
| 1215 | .and_then(NonZeroU16::new) |
| 1216 | // `ttf_parser` guarantees that units_per_em is >= 16 |
| 1217 | .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap()); |
| 1218 | |
| 1219 | (metrics.position, thickness) |
| 1220 | } |
| 1221 | None => ( |
| 1222 | -(units_per_em.get() as i16) / 9, |
| 1223 | NonZeroU16::new(units_per_em.get() / 12).unwrap(), |
| 1224 | ), |
| 1225 | }; |
| 1226 | |
| 1227 | // 0.2 and 0.4 are generic offsets used by some applications (Inkscape/librsvg). |
| 1228 | let mut subscript_offset = (units_per_em.get() as f32 / 0.2).round() as i16; |
| 1229 | let mut superscript_offset = (units_per_em.get() as f32 / 0.4).round() as i16; |
| 1230 | if let Some(metrics) = font.subscript_metrics() { |
| 1231 | subscript_offset = metrics.y_offset; |
| 1232 | } |
| 1233 | |
| 1234 | if let Some(metrics) = font.superscript_metrics() { |
| 1235 | superscript_offset = metrics.y_offset; |
| 1236 | } |
| 1237 | |
| 1238 | Some(ResolvedFont { |
| 1239 | id, |
| 1240 | units_per_em, |
| 1241 | ascent, |
| 1242 | descent, |
| 1243 | x_height, |
| 1244 | underline_position, |
| 1245 | underline_thickness, |
| 1246 | line_through_position, |
| 1247 | subscript_offset, |
| 1248 | superscript_offset, |
| 1249 | }) |
| 1250 | })? |
| 1251 | } |
| 1252 | |
| 1253 | #[inline (never)] |
| 1254 | fn has_char(&self, id: ID, c: char) -> bool { |
| 1255 | let res = self.with_face_data(id, |font_data, face_index| -> Option<bool> { |
| 1256 | let font = ttf_parser::Face::parse(font_data, face_index).ok()?; |
| 1257 | font.glyph_index(c)?; |
| 1258 | Some(true) |
| 1259 | }); |
| 1260 | |
| 1261 | res == Some(Some(true)) |
| 1262 | } |
| 1263 | } |
| 1264 | |
| 1265 | /// Text shaping with font fallback. |
| 1266 | pub(crate) fn shape_text( |
| 1267 | text: &str, |
| 1268 | font: Arc<ResolvedFont>, |
| 1269 | small_caps: bool, |
| 1270 | apply_kerning: bool, |
| 1271 | resolver: &FontResolver, |
| 1272 | fontdb: &mut Arc<fontdb::Database>, |
| 1273 | ) -> Vec<Glyph> { |
| 1274 | let mut glyphs = shape_text_with_font(text, font.clone(), small_caps, apply_kerning, fontdb) |
| 1275 | .unwrap_or_default(); |
| 1276 | |
| 1277 | // Remember all fonts used for shaping. |
| 1278 | let mut used_fonts = vec![font.id]; |
| 1279 | |
| 1280 | // Loop until all glyphs become resolved or until no more fonts are left. |
| 1281 | 'outer: loop { |
| 1282 | let mut missing = None; |
| 1283 | for glyph in &glyphs { |
| 1284 | if glyph.is_missing() { |
| 1285 | missing = Some(glyph.byte_idx.char_from(text)); |
| 1286 | break; |
| 1287 | } |
| 1288 | } |
| 1289 | |
| 1290 | if let Some(c) = missing { |
| 1291 | let fallback_font = match (resolver.select_fallback)(c, &used_fonts, fontdb) |
| 1292 | .and_then(|id| fontdb.load_font(id)) |
| 1293 | { |
| 1294 | Some(v) => Arc::new(v), |
| 1295 | None => break 'outer, |
| 1296 | }; |
| 1297 | |
| 1298 | // Shape again, using a new font. |
| 1299 | let fallback_glyphs = shape_text_with_font( |
| 1300 | text, |
| 1301 | fallback_font.clone(), |
| 1302 | small_caps, |
| 1303 | apply_kerning, |
| 1304 | fontdb, |
| 1305 | ) |
| 1306 | .unwrap_or_default(); |
| 1307 | |
| 1308 | let all_matched = fallback_glyphs.iter().all(|g| !g.is_missing()); |
| 1309 | if all_matched { |
| 1310 | // Replace all glyphs when all of them were matched. |
| 1311 | glyphs = fallback_glyphs; |
| 1312 | break 'outer; |
| 1313 | } |
| 1314 | |
| 1315 | // We assume, that shaping with an any font will produce the same amount of glyphs. |
| 1316 | // This is incorrect, but good enough for now. |
| 1317 | if glyphs.len() != fallback_glyphs.len() { |
| 1318 | break 'outer; |
| 1319 | } |
| 1320 | |
| 1321 | // TODO: Replace clusters and not glyphs. This should be more accurate. |
| 1322 | |
| 1323 | // Copy new glyphs. |
| 1324 | for i in 0..glyphs.len() { |
| 1325 | if glyphs[i].is_missing() && !fallback_glyphs[i].is_missing() { |
| 1326 | glyphs[i] = fallback_glyphs[i].clone(); |
| 1327 | } |
| 1328 | } |
| 1329 | |
| 1330 | // Remember this font. |
| 1331 | used_fonts.push(fallback_font.id); |
| 1332 | } else { |
| 1333 | break 'outer; |
| 1334 | } |
| 1335 | } |
| 1336 | |
| 1337 | // Warn about missing glyphs. |
| 1338 | for glyph in &glyphs { |
| 1339 | if glyph.is_missing() { |
| 1340 | let c = glyph.byte_idx.char_from(text); |
| 1341 | // TODO: print a full grapheme |
| 1342 | log::warn!( |
| 1343 | "No fonts with a {}/U+ {:X} character were found." , |
| 1344 | c, |
| 1345 | c as u32 |
| 1346 | ); |
| 1347 | } |
| 1348 | } |
| 1349 | |
| 1350 | glyphs |
| 1351 | } |
| 1352 | |
| 1353 | /// Converts a text into a list of glyph IDs. |
| 1354 | /// |
| 1355 | /// This function will do the BIDI reordering and text shaping. |
| 1356 | fn shape_text_with_font( |
| 1357 | text: &str, |
| 1358 | font: Arc<ResolvedFont>, |
| 1359 | small_caps: bool, |
| 1360 | apply_kerning: bool, |
| 1361 | fontdb: &fontdb::Database, |
| 1362 | ) -> Option<Vec<Glyph>> { |
| 1363 | fontdb.with_face_data(font.id, |font_data, face_index| -> Option<Vec<Glyph>> { |
| 1364 | let rb_font = rustybuzz::Face::from_slice(font_data, face_index)?; |
| 1365 | |
| 1366 | let bidi_info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr())); |
| 1367 | let paragraph = &bidi_info.paragraphs[0]; |
| 1368 | let line = paragraph.range.clone(); |
| 1369 | |
| 1370 | let mut glyphs = Vec::new(); |
| 1371 | |
| 1372 | let (levels, runs) = bidi_info.visual_runs(paragraph, line); |
| 1373 | for run in runs.iter() { |
| 1374 | let sub_text = &text[run.clone()]; |
| 1375 | if sub_text.is_empty() { |
| 1376 | continue; |
| 1377 | } |
| 1378 | |
| 1379 | let ltr = levels[run.start].is_ltr(); |
| 1380 | let hb_direction = if ltr { |
| 1381 | rustybuzz::Direction::LeftToRight |
| 1382 | } else { |
| 1383 | rustybuzz::Direction::RightToLeft |
| 1384 | }; |
| 1385 | |
| 1386 | let mut buffer = rustybuzz::UnicodeBuffer::new(); |
| 1387 | buffer.push_str(sub_text); |
| 1388 | buffer.set_direction(hb_direction); |
| 1389 | |
| 1390 | let mut features = Vec::new(); |
| 1391 | if small_caps { |
| 1392 | features.push(rustybuzz::Feature::new(Tag::from_bytes(b"smcp" ), 1, ..)); |
| 1393 | } |
| 1394 | |
| 1395 | if !apply_kerning { |
| 1396 | features.push(rustybuzz::Feature::new(Tag::from_bytes(b"kern" ), 0, ..)); |
| 1397 | } |
| 1398 | |
| 1399 | let output = rustybuzz::shape(&rb_font, &features, buffer); |
| 1400 | |
| 1401 | let positions = output.glyph_positions(); |
| 1402 | let infos = output.glyph_infos(); |
| 1403 | |
| 1404 | for i in 0..output.len() { |
| 1405 | let pos = positions[i]; |
| 1406 | let info = infos[i]; |
| 1407 | let idx = run.start + info.cluster as usize; |
| 1408 | |
| 1409 | let start = info.cluster as usize; |
| 1410 | |
| 1411 | let end = if ltr { |
| 1412 | i.checked_add(1) |
| 1413 | } else { |
| 1414 | i.checked_sub(1) |
| 1415 | } |
| 1416 | .and_then(|last| infos.get(last)) |
| 1417 | .map_or(sub_text.len(), |info| info.cluster as usize); |
| 1418 | |
| 1419 | glyphs.push(Glyph { |
| 1420 | byte_idx: ByteIndex::new(idx), |
| 1421 | cluster_len: end.checked_sub(start).unwrap_or(0), // TODO: can fail? |
| 1422 | text: sub_text[start..end].to_string(), |
| 1423 | id: GlyphId(info.glyph_id as u16), |
| 1424 | dx: pos.x_offset, |
| 1425 | dy: pos.y_offset, |
| 1426 | width: pos.x_advance, |
| 1427 | font: font.clone(), |
| 1428 | }); |
| 1429 | } |
| 1430 | } |
| 1431 | |
| 1432 | Some(glyphs) |
| 1433 | })? |
| 1434 | } |
| 1435 | |
| 1436 | /// An iterator over glyph clusters. |
| 1437 | /// |
| 1438 | /// Input: 0 2 2 2 3 4 4 5 5 |
| 1439 | /// Result: 0 1 4 5 7 |
| 1440 | pub(crate) struct GlyphClusters<'a> { |
| 1441 | data: &'a [Glyph], |
| 1442 | idx: usize, |
| 1443 | } |
| 1444 | |
| 1445 | impl<'a> GlyphClusters<'a> { |
| 1446 | pub(crate) fn new(data: &'a [Glyph]) -> Self { |
| 1447 | GlyphClusters { data, idx: 0 } |
| 1448 | } |
| 1449 | } |
| 1450 | |
| 1451 | impl Iterator for GlyphClusters<'_> { |
| 1452 | type Item = (std::ops::Range<usize>, ByteIndex); |
| 1453 | |
| 1454 | fn next(&mut self) -> Option<Self::Item> { |
| 1455 | if self.idx == self.data.len() { |
| 1456 | return None; |
| 1457 | } |
| 1458 | |
| 1459 | let start: usize = self.idx; |
| 1460 | let cluster: ByteIndex = self.data[self.idx].byte_idx; |
| 1461 | for g: &Glyph in &self.data[self.idx..] { |
| 1462 | if g.byte_idx != cluster { |
| 1463 | break; |
| 1464 | } |
| 1465 | |
| 1466 | self.idx += 1; |
| 1467 | } |
| 1468 | |
| 1469 | Some((start..self.idx, cluster)) |
| 1470 | } |
| 1471 | } |
| 1472 | |
| 1473 | /// Checks that selected script supports letter spacing. |
| 1474 | /// |
| 1475 | /// [In the CSS spec](https://www.w3.org/TR/css-text-3/#cursive-tracking). |
| 1476 | /// |
| 1477 | /// The list itself is from: https://github.com/harfbuzz/harfbuzz/issues/64 |
| 1478 | pub(crate) fn script_supports_letter_spacing(script: unicode_script::Script) -> bool { |
| 1479 | use unicode_script::Script; |
| 1480 | |
| 1481 | !matches!( |
| 1482 | script, |
| 1483 | Script::Arabic |
| 1484 | | Script::Syriac |
| 1485 | | Script::Nko |
| 1486 | | Script::Manichaean |
| 1487 | | Script::Psalter_Pahlavi |
| 1488 | | Script::Mandaic |
| 1489 | | Script::Mongolian |
| 1490 | | Script::Phags_Pa |
| 1491 | | Script::Devanagari |
| 1492 | | Script::Bengali |
| 1493 | | Script::Gurmukhi |
| 1494 | | Script::Modi |
| 1495 | | Script::Sharada |
| 1496 | | Script::Syloti_Nagri |
| 1497 | | Script::Tirhuta |
| 1498 | | Script::Ogham |
| 1499 | ) |
| 1500 | } |
| 1501 | |
| 1502 | /// A glyph. |
| 1503 | /// |
| 1504 | /// Basically, a glyph ID and it's metrics. |
| 1505 | #[derive (Clone)] |
| 1506 | pub(crate) struct Glyph { |
| 1507 | /// The glyph ID in the font. |
| 1508 | pub(crate) id: GlyphId, |
| 1509 | |
| 1510 | /// Position in bytes in the original string. |
| 1511 | /// |
| 1512 | /// We use it to match a glyph with a character in the text chunk and therefore with the style. |
| 1513 | pub(crate) byte_idx: ByteIndex, |
| 1514 | |
| 1515 | // The length of the cluster in bytes. |
| 1516 | pub(crate) cluster_len: usize, |
| 1517 | |
| 1518 | /// The text from the original string that corresponds to that glyph. |
| 1519 | pub(crate) text: String, |
| 1520 | |
| 1521 | /// The glyph offset in font units. |
| 1522 | pub(crate) dx: i32, |
| 1523 | |
| 1524 | /// The glyph offset in font units. |
| 1525 | pub(crate) dy: i32, |
| 1526 | |
| 1527 | /// The glyph width / X-advance in font units. |
| 1528 | pub(crate) width: i32, |
| 1529 | |
| 1530 | /// Reference to the source font. |
| 1531 | /// |
| 1532 | /// Each glyph can have it's own source font. |
| 1533 | pub(crate) font: Arc<ResolvedFont>, |
| 1534 | } |
| 1535 | |
| 1536 | impl Glyph { |
| 1537 | fn is_missing(&self) -> bool { |
| 1538 | self.id.0 == 0 |
| 1539 | } |
| 1540 | } |
| 1541 | |
| 1542 | #[derive (Clone, Copy, Debug)] |
| 1543 | pub(crate) struct ResolvedFont { |
| 1544 | pub(crate) id: ID, |
| 1545 | |
| 1546 | units_per_em: NonZeroU16, |
| 1547 | |
| 1548 | // All values below are in font units. |
| 1549 | ascent: i16, |
| 1550 | descent: i16, |
| 1551 | x_height: NonZeroU16, |
| 1552 | |
| 1553 | underline_position: i16, |
| 1554 | underline_thickness: NonZeroU16, |
| 1555 | |
| 1556 | // line-through thickness should be the the same as underline thickness |
| 1557 | // according to the TrueType spec: |
| 1558 | // https://docs.microsoft.com/en-us/typography/opentype/spec/os2#ystrikeoutsize |
| 1559 | line_through_position: i16, |
| 1560 | |
| 1561 | subscript_offset: i16, |
| 1562 | superscript_offset: i16, |
| 1563 | } |
| 1564 | |
| 1565 | pub(crate) fn chunk_span_at(chunk: &TextChunk, byte_offset: ByteIndex) -> Option<&TextSpan> { |
| 1566 | chunkIter<'_, TextSpan> |
| 1567 | .spans |
| 1568 | .iter() |
| 1569 | .find(|&span: &TextSpan| span_contains(span, byte_offset)) |
| 1570 | } |
| 1571 | |
| 1572 | pub(crate) fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool { |
| 1573 | byte_offset.value() >= span.start && byte_offset.value() < span.end |
| 1574 | } |
| 1575 | |
| 1576 | /// Checks that the selected character is a word separator. |
| 1577 | /// |
| 1578 | /// According to: https://www.w3.org/TR/css-text-3/#word-separator |
| 1579 | pub(crate) fn is_word_separator_characters(c: char) -> bool { |
| 1580 | matches!( |
| 1581 | c as u32, |
| 1582 | 0x0020 | 0x00A0 | 0x1361 | 0x010100 | 0x010101 | 0x01039F | 0x01091F |
| 1583 | ) |
| 1584 | } |
| 1585 | |
| 1586 | impl ResolvedFont { |
| 1587 | #[inline ] |
| 1588 | pub(crate) fn scale(&self, font_size: f32) -> f32 { |
| 1589 | font_size / self.units_per_em.get() as f32 |
| 1590 | } |
| 1591 | |
| 1592 | #[inline ] |
| 1593 | pub(crate) fn ascent(&self, font_size: f32) -> f32 { |
| 1594 | self.ascent as f32 * self.scale(font_size) |
| 1595 | } |
| 1596 | |
| 1597 | #[inline ] |
| 1598 | pub(crate) fn descent(&self, font_size: f32) -> f32 { |
| 1599 | self.descent as f32 * self.scale(font_size) |
| 1600 | } |
| 1601 | |
| 1602 | #[inline ] |
| 1603 | pub(crate) fn height(&self, font_size: f32) -> f32 { |
| 1604 | self.ascent(font_size) - self.descent(font_size) |
| 1605 | } |
| 1606 | |
| 1607 | #[inline ] |
| 1608 | pub(crate) fn x_height(&self, font_size: f32) -> f32 { |
| 1609 | self.x_height.get() as f32 * self.scale(font_size) |
| 1610 | } |
| 1611 | |
| 1612 | #[inline ] |
| 1613 | pub(crate) fn underline_position(&self, font_size: f32) -> f32 { |
| 1614 | self.underline_position as f32 * self.scale(font_size) |
| 1615 | } |
| 1616 | |
| 1617 | #[inline ] |
| 1618 | fn underline_thickness(&self, font_size: f32) -> f32 { |
| 1619 | self.underline_thickness.get() as f32 * self.scale(font_size) |
| 1620 | } |
| 1621 | |
| 1622 | #[inline ] |
| 1623 | pub(crate) fn line_through_position(&self, font_size: f32) -> f32 { |
| 1624 | self.line_through_position as f32 * self.scale(font_size) |
| 1625 | } |
| 1626 | |
| 1627 | #[inline ] |
| 1628 | fn subscript_offset(&self, font_size: f32) -> f32 { |
| 1629 | self.subscript_offset as f32 * self.scale(font_size) |
| 1630 | } |
| 1631 | |
| 1632 | #[inline ] |
| 1633 | fn superscript_offset(&self, font_size: f32) -> f32 { |
| 1634 | self.superscript_offset as f32 * self.scale(font_size) |
| 1635 | } |
| 1636 | |
| 1637 | fn dominant_baseline_shift(&self, baseline: DominantBaseline, font_size: f32) -> f32 { |
| 1638 | let alignment = match baseline { |
| 1639 | DominantBaseline::Auto => AlignmentBaseline::Auto, |
| 1640 | DominantBaseline::UseScript => AlignmentBaseline::Auto, // unsupported |
| 1641 | DominantBaseline::NoChange => AlignmentBaseline::Auto, // already resolved |
| 1642 | DominantBaseline::ResetSize => AlignmentBaseline::Auto, // unsupported |
| 1643 | DominantBaseline::Ideographic => AlignmentBaseline::Ideographic, |
| 1644 | DominantBaseline::Alphabetic => AlignmentBaseline::Alphabetic, |
| 1645 | DominantBaseline::Hanging => AlignmentBaseline::Hanging, |
| 1646 | DominantBaseline::Mathematical => AlignmentBaseline::Mathematical, |
| 1647 | DominantBaseline::Central => AlignmentBaseline::Central, |
| 1648 | DominantBaseline::Middle => AlignmentBaseline::Middle, |
| 1649 | DominantBaseline::TextAfterEdge => AlignmentBaseline::TextAfterEdge, |
| 1650 | DominantBaseline::TextBeforeEdge => AlignmentBaseline::TextBeforeEdge, |
| 1651 | }; |
| 1652 | |
| 1653 | self.alignment_baseline_shift(alignment, font_size) |
| 1654 | } |
| 1655 | |
| 1656 | // The `alignment-baseline` property is a mess. |
| 1657 | // |
| 1658 | // The SVG 1.1 spec (https://www.w3.org/TR/SVG11/text.html#BaselineAlignmentProperties) |
| 1659 | // goes on and on about what this property suppose to do, but doesn't actually explain |
| 1660 | // how it should be implemented. It's just a very verbose overview. |
| 1661 | // |
| 1662 | // As of Nov 2022, only Chrome and Safari support `alignment-baseline`. Firefox isn't. |
| 1663 | // Same goes for basically every SVG library in existence. |
| 1664 | // Meaning we have no idea how exactly it should be implemented. |
| 1665 | // |
| 1666 | // And even Chrome and Safari cannot agree on how to handle `baseline`, `after-edge`, |
| 1667 | // `text-after-edge` and `ideographic` variants. Producing vastly different output. |
| 1668 | // |
| 1669 | // As per spec, a proper implementation should get baseline values from the font itself, |
| 1670 | // using `BASE` and `bsln` TrueType tables. If those tables are not present, |
| 1671 | // we have to synthesize them (https://drafts.csswg.org/css-inline/#baseline-synthesis-fonts). |
| 1672 | // And in the worst case scenario simply fallback to hardcoded values. |
| 1673 | // |
| 1674 | // Also, most fonts do not provide `BASE` and `bsln` tables to begin with. |
| 1675 | // |
| 1676 | // Again, as of Nov 2022, Chrome does only the latter: |
| 1677 | // https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/platform/fonts/font_metrics.cc#L153 |
| 1678 | // |
| 1679 | // Since baseline TrueType tables parsing and baseline synthesis are pretty hard, |
| 1680 | // we do what Chrome does - use hardcoded values. And it seems like Safari does the same. |
| 1681 | // |
| 1682 | // |
| 1683 | // But that's not all! SVG 2 and CSS Inline Layout 3 did a baseline handling overhaul, |
| 1684 | // and it's far more complex now. Not sure if anyone actually supports it. |
| 1685 | fn alignment_baseline_shift(&self, alignment: AlignmentBaseline, font_size: f32) -> f32 { |
| 1686 | match alignment { |
| 1687 | AlignmentBaseline::Auto => 0.0, |
| 1688 | AlignmentBaseline::Baseline => 0.0, |
| 1689 | AlignmentBaseline::BeforeEdge | AlignmentBaseline::TextBeforeEdge => { |
| 1690 | self.ascent(font_size) |
| 1691 | } |
| 1692 | AlignmentBaseline::Middle => self.x_height(font_size) * 0.5, |
| 1693 | AlignmentBaseline::Central => self.ascent(font_size) - self.height(font_size) * 0.5, |
| 1694 | AlignmentBaseline::AfterEdge | AlignmentBaseline::TextAfterEdge => { |
| 1695 | self.descent(font_size) |
| 1696 | } |
| 1697 | AlignmentBaseline::Ideographic => self.descent(font_size), |
| 1698 | AlignmentBaseline::Alphabetic => 0.0, |
| 1699 | AlignmentBaseline::Hanging => self.ascent(font_size) * 0.8, |
| 1700 | AlignmentBaseline::Mathematical => self.ascent(font_size) * 0.5, |
| 1701 | } |
| 1702 | } |
| 1703 | } |
| 1704 | |
| 1705 | pub(crate) type FontsCache = HashMap<Font, Arc<ResolvedFont>>; |
| 1706 | |
| 1707 | /// A read-only text index in bytes. |
| 1708 | /// |
| 1709 | /// Guarantee to be on a char boundary and in text bounds. |
| 1710 | #[derive (Clone, Copy, PartialEq, Debug)] |
| 1711 | pub(crate) struct ByteIndex(usize); |
| 1712 | |
| 1713 | impl ByteIndex { |
| 1714 | fn new(i: usize) -> Self { |
| 1715 | ByteIndex(i) |
| 1716 | } |
| 1717 | |
| 1718 | pub(crate) fn value(&self) -> usize { |
| 1719 | self.0 |
| 1720 | } |
| 1721 | |
| 1722 | /// Converts byte position into a code point position. |
| 1723 | pub(crate) fn code_point_at(&self, text: &str) -> usize { |
| 1724 | textimpl Iterator .char_indices() |
| 1725 | .take_while(|(i: &usize, _)| *i != self.0) |
| 1726 | .count() |
| 1727 | } |
| 1728 | |
| 1729 | /// Converts byte position into a character. |
| 1730 | pub(crate) fn char_from(&self, text: &str) -> char { |
| 1731 | text[self.0..].chars().next().unwrap() |
| 1732 | } |
| 1733 | } |
| 1734 | |