#![cfg_attr(docsrs, feature(doc_cfg))]
use std::{
borrow::Cow,
cmp,
collections::{BTreeSet, HashMap},
};
pub use exemplars::{CollectToExemplars, Exemplars};
use harfrust::{Shaper, ShaperData, ShaperInstance, UnicodeBuffer};
pub use harfshapedfa::Location;
use harfshapedfa::{HarfRustShaperExt, ShapingMeta, pens::BoundsPen};
use itertools::Itertools;
use ordered_float::{NotNan, OrderedFloat};
use skrifa::{
FontRef, MetadataProvider, instance::Size, outline::DrawSettings,
};
pub use static_lang_word_lists::WordList;
use static_lang_word_lists::WordListIter;
use crate::errors::{
FontHeightError, SkrifaDrawError, SkrifaReadError, WordListShapingPlanError,
};
pub mod errors;
mod exemplars;
pub struct Reporter<'a> {
font: FontRef<'a>,
shaper_data: ShaperData,
}
impl<'a> Reporter<'a> {
pub fn new(font_bytes: &'a [u8]) -> Result<Self, FontHeightError> {
let font = FontRef::new(font_bytes).map_err(SkrifaReadError::from)?;
Ok(Reporter {
shaper_data: ShaperData::new(&font),
font,
})
}
#[inline]
#[must_use]
pub const fn fontref(&self) -> &FontRef<'_> {
&self.font
}
#[must_use]
pub fn interesting_locations(&self) -> Vec<Location> {
let mut axis_coords =
vec![BTreeSet::<OrderedFloat<f32>>::new(); self.font.axes().len()];
self.font
.named_instances()
.iter()
.flat_map(|instance| instance.user_coords().enumerate())
.for_each(|(axis, coord)| {
axis_coords[axis].insert(coord.into());
});
self.font.axes().iter().for_each(|axis| {
axis_coords[axis.index()].extend(&[
axis.default_value().into(),
axis.min_value().into(),
axis.max_value().into(),
]);
});
axis_coords
.iter()
.multi_cartesian_product()
.map(|coords| {
self.font
.axes()
.iter()
.zip(coords)
.map(|(axis, coord)| (axis.tag(), coord.into_inner()))
.collect()
})
.collect()
}
pub fn instance(
&'a self,
location: &'a Location,
) -> Result<InstanceReporter<'a>, FontHeightError> {
let instance_extremes = InstanceExtremes::new(&self.font, location)?;
let shaper_instance =
ShaperInstance::from_variations(&self.font, location.to_harfrust());
Ok(InstanceReporter {
font: &self.font,
location: Cow::Borrowed(location),
shaper_data: &self.shaper_data,
shaper_instance,
instance_extremes,
})
}
pub fn default_instance(
&'a self,
) -> Result<InstanceReporter<'a>, SkrifaDrawError> {
let location = Cow::<Location>::default();
let instance_extremes = InstanceExtremes::new(&self.font, &location)
.map_err(|err| {
let FontHeightError::Drawing(draw_err) = err else {
unreachable!(
"InstanceExtremes with a known-good location returned \
an error that wasn't a SkrifaDrawError"
);
};
draw_err
})?;
let shaper_instance =
ShaperInstance::from_variations(&self.font, location.to_harfrust());
Ok(InstanceReporter {
font: &self.font,
location,
shaper_data: &self.shaper_data,
shaper_instance,
instance_extremes,
})
}
}
pub struct InstanceReporter<'a> {
font: &'a FontRef<'a>,
location: Cow<'a, Location>,
shaper_data: &'a ShaperData,
shaper_instance: ShaperInstance,
instance_extremes: InstanceExtremes,
}
impl<'a> InstanceReporter<'a> {
#[inline]
#[must_use]
pub fn location(&self) -> &Location {
self.location.as_ref()
}
pub fn to_word_extremes_iter(
&self,
word_list: &'a WordList,
) -> Result<WordExtremesIterator<'_>, WordListShapingPlanError> {
let shaper = self
.shaper_data
.shaper(self.font)
.instance(Some(&self.shaper_instance))
.build();
let shaping_meta = word_list
.script()
.map(|script| {
ShapingMeta::new(script, word_list.language(), &shaper)
})
.transpose()
.map_err(|err| WordListShapingPlanError {
word_list_name: word_list.name().to_owned(),
inner: err,
})?;
Ok(WordExtremesIterator {
shaper,
instance_extremes: &self.instance_extremes,
shaping_meta,
word_iter: word_list.iter(),
unicode_buffer: Some(UnicodeBuffer::new()),
})
}
#[cfg(feature = "rayon")]
pub fn par_check(
&'a self,
word_list: &'a WordList,
k_words: Option<usize>,
n_exemplars: usize,
) -> Result<Report<'a>, WordListShapingPlanError> {
use std::convert::identity;
use exemplars::ExemplarCollector;
use rayon::prelude::*;
struct WorkerState {
unicode_buffer: Option<UnicodeBuffer>,
}
let shaper = self
.shaper_data
.shaper(self.font)
.instance(Some(&self.shaper_instance))
.build();
let shaping_meta = word_list
.script()
.map(|script| {
ShapingMeta::new(script, word_list.language(), &shaper)
})
.transpose()
.map_err(|err| WordListShapingPlanError {
word_list_name: word_list.name().to_owned(),
inner: err,
})?;
let exemplars = word_list
.par_iter()
.take(k_words.unwrap_or(usize::MAX))
.map_init(
|| WorkerState {
unicode_buffer: Some(UnicodeBuffer::new()),
},
|state, word| {
let mut buffer = state.unicode_buffer.take().unwrap();
buffer.push_str(word);
let glyph_buffer = match &shaping_meta {
Some(meta) => shaper.shape_with_meta(meta, buffer, &[]),
None => {
buffer.guess_segment_properties();
shaper.shape(buffer, &[])
},
};
let glyphs_missing = glyph_buffer
.glyph_infos()
.iter()
.any(|info| info.glyph_id == 0); if glyphs_missing {
state.unicode_buffer = Some(glyph_buffer.clear());
return None;
}
let extremes = glyph_buffer
.glyph_infos()
.iter()
.zip(glyph_buffer.glyph_positions())
.map(|(info, pos)| {
let y_offset = NotNan::new(pos.y_offset as f64)
.expect("NaN y offset");
let heights = self
.instance_extremes
.get(info.glyph_id)
.unwrap();
VerticalExtremes {
lowest: heights.lowest + y_offset,
highest: heights.highest + y_offset,
}
})
.reduce(VerticalExtremes::merge)
.unwrap_or_default();
state.unicode_buffer = Some(glyph_buffer.clear());
Some(WordExtremes { word, extremes })
},
)
.filter_map(identity)
.fold(
|| ExemplarCollector::new(n_exemplars),
|mut acc, word_extremes| {
acc.push(word_extremes);
acc
},
)
.reduce(
|| ExemplarCollector::new(n_exemplars),
|mut acc, other| {
acc.merge_with(other);
acc
},
)
.build();
Ok(Report {
location: self.location.as_ref(),
word_list,
exemplars,
})
}
}
pub struct WordExtremesIterator<'a> {
shaper: Shaper<'a>,
instance_extremes: &'a InstanceExtremes,
shaping_meta: Option<ShapingMeta>,
word_iter: WordListIter<'a>,
unicode_buffer: Option<UnicodeBuffer>,
}
impl<'a> Iterator for WordExtremesIterator<'a> {
type Item = WordExtremes<'a>;
fn next(&mut self) -> Option<Self::Item> {
debug_assert!(
self.unicode_buffer.is_some(),
"WordExtremesIterator.unicode_buffer wasn't stored back in self \
during the previous iteration"
);
let (word, glyph_buffer) = self.word_iter.find_map(|word| {
let mut buffer = self.unicode_buffer.take().unwrap();
buffer.push_str(word);
let glyph_buffer = match &self.shaping_meta {
Some(meta) => self.shaper.shape_with_meta(meta, buffer, &[]),
None => {
buffer.guess_segment_properties();
self.shaper.shape(buffer, &[])
},
};
let glyphs_missing = glyph_buffer
.glyph_infos()
.iter()
.any(|info| info.glyph_id == 0);
if !glyphs_missing {
Some((word, glyph_buffer))
} else {
self.unicode_buffer = Some(glyph_buffer.clear());
None
}
})?;
let word_extremes = glyph_buffer
.glyph_infos()
.iter()
.zip(glyph_buffer.glyph_positions())
.map(|(info, pos)| {
let y_offset =
NotNan::new(pos.y_offset as f64).expect("NaN y offset");
let heights =
self.instance_extremes.get(info.glyph_id).unwrap();
VerticalExtremes {
lowest: heights.lowest + y_offset,
highest: heights.highest + y_offset,
}
})
.reduce(VerticalExtremes::merge)
.unwrap_or_default();
self.unicode_buffer = Some(glyph_buffer.clear());
Some(WordExtremes {
word,
extremes: word_extremes,
})
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct WordExtremes<'w> {
pub word: &'w str,
pub extremes: VerticalExtremes,
}
impl WordExtremes<'_> {
#[inline]
#[must_use]
pub fn lowest(&self) -> f64 {
self.extremes.lowest()
}
#[inline]
#[must_use]
pub fn highest(&self) -> f64 {
self.extremes.highest()
}
#[inline]
#[must_use]
pub fn lower(self, other: Self) -> Self {
if self.extremes.lowest <= other.extremes.lowest {
self
} else {
other
}
}
#[inline]
#[must_use]
pub fn higher(self, other: Self) -> Self {
if self.extremes.highest >= other.extremes.highest {
self
} else {
other
}
}
}
#[derive(Debug)]
pub(crate) struct InstanceExtremes(HashMap<u32, VerticalExtremes>);
impl InstanceExtremes {
pub fn new(
font: &FontRef,
location: &Location,
) -> Result<Self, FontHeightError> {
location.validate_for(font)?;
let instance_extremes = font
.outline_glyphs()
.iter()
.map(|(id, outline)| -> Result<(u32, VerticalExtremes), SkrifaDrawError> {
let mut bounds_pen = BoundsPen::new();
outline
.draw(
DrawSettings::unhinted(
Size::unscaled(),
&location.to_skrifa(font),
),
&mut bounds_pen,
)
.map_err(|err| SkrifaDrawError(id, err))?;
let harfshapedfa::kurbo::Rect { y0, y1, .. } = bounds_pen.bounds();
Ok((u32::from(id), VerticalExtremes {
lowest: NotNan::new(y0).expect("bounding box with NaN y0"),
highest: NotNan::new(y1).expect("bounding box with NaN y1"),
}))
})
.collect::<Result<HashMap<_, _>, _>>()?;
Ok(InstanceExtremes(instance_extremes))
}
#[must_use]
pub fn get(&self, glyph_id: u32) -> Option<VerticalExtremes> {
self.0.get(&glyph_id).copied()
}
}
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash)]
pub struct VerticalExtremes {
lowest: NotNan<f64>,
highest: NotNan<f64>,
}
impl VerticalExtremes {
#[inline]
#[must_use]
pub fn new(lowest: f64, highest: f64) -> Self {
let lowest = NotNan::new(lowest).expect("lowest was NaN");
let highest = NotNan::new(highest).expect("highest was NaN");
assert!(
lowest <= highest,
"lowest value was greater than highest value"
);
Self { lowest, highest }
}
#[inline]
#[must_use]
pub fn lowest(&self) -> f64 {
*self.lowest
}
#[inline]
#[must_use]
pub fn highest(&self) -> f64 {
*self.highest
}
#[inline]
#[must_use]
pub fn merge(self, other: Self) -> Self {
Self {
lowest: cmp::min(self.lowest, other.lowest),
highest: cmp::max(self.highest, other.highest),
}
}
}
#[derive(Debug, Clone)]
pub struct Report<'a> {
pub location: &'a Location,
pub word_list: &'a WordList,
pub exemplars: Exemplars<'a>,
}
impl<'a> Report<'a> {
#[inline]
#[must_use]
pub const fn new(
location: &'a Location,
word_list: &'a WordList,
exemplars: Exemplars<'a>,
) -> Self {
Report {
location,
word_list,
exemplars,
}
}
}