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