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 | |