use std::collections::HashMap;
use crate::layout::engine::TextRun;
use crate::parser::ttf::TtfFont;
use crate::types::Point;
use super::{DecorationLine, InlineInterval, merge_intervals, thickness};
pub(crate) fn ink_skip_intervals(
run: &TextRun,
decoration: &crate::style::computed::TextDecoration,
line: DecorationLine,
axis_from_baseline: f32,
custom_fonts: &HashMap<String, TtfFont>,
) -> Vec<InlineInterval> {
use crate::style::computed::TextDecorationSkipInk;
if !line.can_skip_ink() || decoration.skip_ink == TextDecorationSkipInk::None {
return Vec::new();
}
let Some((_, font)) = crate::text::resolve_custom_font(
&run.font_family,
run.bold,
run.font_style.is_slanted(),
custom_fonts,
) else {
return Vec::new();
};
let Some(shaped) = crate::text::shape_text_run(run, custom_fonts) else {
return Vec::new();
};
let Ok(face) = rustybuzz::ttf_parser::Face::parse(&font.data, font.face_index.get()) else {
return Vec::new();
};
let units_per_em = f32::from(face.units_per_em()).max(1.0);
let scale = font.adjusted_font_size(run.font_size) / units_per_em;
let thickness = thickness(run, decoration);
let synthetic_bold = run
.synthetic_bold_stroke_width(custom_fonts)
.unwrap_or_default()
/ 2.0;
let collision_half_band = thickness * 0.1 + synthetic_bold;
let shear = run.synthetic_italic_shear(custom_fonts).unwrap_or_default();
let clearance = crate::fonts::PT_PER_CSS_PX / 2.0 + thickness / 2.0;
let mut cursor = 0.0;
let mut intervals = Vec::new();
for glyph in &shaped.glyphs {
let skip_for_script = decoration.skip_ink == TextDecorationSkipInk::All
|| !glyph_unicode_is_cjk(&glyph.unicode);
if skip_for_script {
let mut outline = GlyphOutline::default();
if face
.outline_glyph(rustybuzz::ttf_parser::GlyphId(glyph.glyph_id), &mut outline)
.is_some()
{
outline.finish();
let band = InlineInterval::new(
(axis_from_baseline - glyph.y_offset - collision_half_band) / scale,
(axis_from_baseline - glyph.y_offset + collision_half_band) / scale,
);
let middle_y = (band.start + band.end) / 2.0;
intervals.extend(
outline
.horizontal_ink_intervals(band)
.into_iter()
.map(|ink| {
InlineInterval::new(
cursor + glyph.x_offset + (ink.start + shear * middle_y) * scale
- synthetic_bold
- clearance,
cursor
+ glyph.x_offset
+ (ink.end + shear * middle_y) * scale
+ synthetic_bold
+ clearance,
)
}),
);
}
}
cursor += glyph.x_advance;
}
merge_intervals(intervals)
}
fn glyph_unicode_is_cjk(unicode: &[u16]) -> bool {
char::decode_utf16(unicode.iter().copied())
.filter_map(Result::ok)
.any(|ch| {
matches!(
u32::from(ch),
0x3040..=0x30ff
| 0x3100..=0x312f
| 0x31a0..=0x31bf
| 0x3400..=0x4dbf
| 0x4e00..=0x9fff
| 0xf900..=0xfaff
| 0xac00..=0xd7af
)
})
}
#[derive(Default)]
struct GlyphOutline {
contours: Vec<Vec<Point>>,
current: Vec<Point>,
}
impl GlyphOutline {
const CURVE_STEPS: usize = 12;
const BAND_SAMPLES: usize = 5;
fn finish(&mut self) {
self.finish_contour();
}
fn finish_contour(&mut self) {
if self.current.len() < 2 {
self.current.clear();
return;
}
if self.current.first() != self.current.last()
&& let Some(first) = self.current.first().copied()
{
self.current.push(first);
}
self.contours.push(std::mem::take(&mut self.current));
}
fn push_curve(&mut self, controls: &[Point]) {
let Some(start) = self.current.last().copied() else {
return;
};
for step in 1..=Self::CURVE_STEPS {
let t = step as f32 / Self::CURVE_STEPS as f32;
let point = match controls {
[control, end] => quadratic_point(start, *control, *end, t),
[control_1, control_2, end] => cubic_point(start, *control_1, *control_2, *end, t),
_ => return,
};
self.current.push(point);
}
}
fn horizontal_ink_intervals(&self, band: InlineInterval) -> Vec<InlineInterval> {
if !band.start.is_finite() || !band.end.is_finite() || band.end < band.start {
return Vec::new();
}
let mut intervals = Vec::new();
for sample in 0..Self::BAND_SAMPLES {
let ratio = sample as f32 / (Self::BAND_SAMPLES - 1) as f32;
let y = band.start + (band.end - band.start) * ratio;
let mut intersections = self
.contours
.iter()
.flat_map(|contour| contour.windows(2))
.filter_map(|edge| scanline_intersection(edge[0], edge[1], y))
.collect::<Vec<_>>();
intersections.sort_by(f32::total_cmp);
intervals.extend(
intersections
.chunks_exact(2)
.map(|pair| InlineInterval::new(pair[0], pair[1])),
);
}
merge_intervals(intervals)
}
}
impl rustybuzz::ttf_parser::OutlineBuilder for GlyphOutline {
fn move_to(&mut self, x: f32, y: f32) {
self.finish_contour();
self.current.push(Point::new(x, y));
}
fn line_to(&mut self, x: f32, y: f32) {
self.current.push(Point::new(x, y));
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.push_curve(&[Point::new(x1, y1), Point::new(x, y)]);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.push_curve(&[Point::new(x1, y1), Point::new(x2, y2), Point::new(x, y)]);
}
fn close(&mut self) {
self.finish_contour();
}
}
fn scanline_intersection(start: Point, end: Point, y: f32) -> Option<f32> {
let crosses = (start.y <= y && end.y > y) || (end.y <= y && start.y > y);
if !crosses {
return None;
}
let ratio = (y - start.y) / (end.y - start.y);
Some(start.x + (end.x - start.x) * ratio)
}
fn quadratic_point(start: Point, control: Point, end: Point, t: f32) -> Point {
let inverse = 1.0 - t;
Point::new(
inverse * inverse * start.x + 2.0 * inverse * t * control.x + t * t * end.x,
inverse * inverse * start.y + 2.0 * inverse * t * control.y + t * t * end.y,
)
}
fn cubic_point(start: Point, control_1: Point, control_2: Point, end: Point, t: f32) -> Point {
let inverse = 1.0 - t;
Point::new(
inverse.powi(3) * start.x
+ 3.0 * inverse * inverse * t * control_1.x
+ 3.0 * inverse * t * t * control_2.x
+ t.powi(3) * end.x,
inverse.powi(3) * start.y
+ 3.0 * inverse * inverse * t * control_1.y
+ 3.0 * inverse * t * t * control_2.y
+ t.powi(3) * end.y,
)
}